mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-29 05:41:40 +00:00
Compare commits
97 Commits
ashleyst/r
...
release/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7224dd26c1 | ||
|
|
4c73a1cc47 | ||
|
|
610da6a9a5 | ||
|
|
7812ca4914 | ||
|
|
bc4f18ba79 | ||
|
|
fd2551423d | ||
|
|
508abcd21c | ||
|
|
7774589d60 | ||
|
|
e23ba5ec8c | ||
|
|
75719b3cf0 | ||
|
|
f3f8fd241a | ||
|
|
2dc2e59162 | ||
|
|
4b65760a1d | ||
|
|
ced2725476 | ||
|
|
6b811b5e76 | ||
|
|
69cf523274 | ||
|
|
2e45d8a2a4 | ||
|
|
b5d7423849 | ||
|
|
1529303107 | ||
|
|
8624bf0423 | ||
|
|
db1600d81b | ||
|
|
176bb47cb5 | ||
|
|
b6d17284b5 | ||
|
|
7b7a2817b6 | ||
|
|
083bccfda9 | ||
|
|
14c9874e5e | ||
|
|
a04eaff6be | ||
|
|
51a412e2c0 | ||
|
|
f0e32491d7 | ||
|
|
3fcbdf6152 | ||
|
|
8da078579e | ||
|
|
4ac41031e6 | ||
|
|
c33c497fd9 | ||
|
|
d7923db108 | ||
|
|
cec621443d | ||
|
|
b8017763b7 | ||
|
|
59619a856e | ||
|
|
0170c9e1cc | ||
|
|
b1f016a796 | ||
|
|
ae3912cbf2 | ||
|
|
2730da7ab6 | ||
|
|
de2449ee25 | ||
|
|
99378582ce | ||
|
|
bd592d07af | ||
|
|
644f5941ec | ||
|
|
9fb006a996 | ||
|
|
c2b98c3e23 | ||
|
|
76d49d86d4 | ||
|
|
7893b89bf7 | ||
|
|
5945e3cb6b | ||
|
|
213d1c68fe | ||
|
|
c26f9a1ebb | ||
|
|
bd7cd7ae8f | ||
|
|
6504358580 | ||
|
|
ce88659fca | ||
|
|
642c708e9c | ||
|
|
4156009d09 | ||
|
|
5c6abbd635 | ||
|
|
881726e9af | ||
|
|
7015590d1a | ||
|
|
1d952a4ea2 | ||
|
|
2a81551a60 | ||
|
|
eceee36913 | ||
|
|
96faf92c12 | ||
|
|
2fdb3df4ae | ||
|
|
7c9802c07d | ||
|
|
e5609bd91e | ||
|
|
4b75e86b74 | ||
|
|
abf061089d | ||
|
|
ec25586a6e | ||
|
|
c15d1432b2 | ||
|
|
73d2686025 | ||
|
|
80b926214b | ||
|
|
070b7a4ca7 | ||
|
|
d61ff5dcb5 | ||
|
|
d42eebaa5a | ||
|
|
056be2a74d | ||
|
|
b93c90e7d1 | ||
|
|
82de81f2b6 | ||
|
|
236f075cf6 | ||
|
|
d478af3869 | ||
|
|
93c1fdc238 | ||
|
|
d562fc0f40 | ||
|
|
808faa9fa5 | ||
|
|
c1bc11d27d | ||
|
|
ac2e2a6f8e | ||
|
|
3138580eae | ||
|
|
aa88815c6e | ||
|
|
5a2f78b51e | ||
|
|
fbc2e1335b | ||
|
|
eb0d7b71b3 | ||
|
|
261289b031 | ||
|
|
fae4589427 | ||
|
|
cbcb7e6240 | ||
|
|
e0b773d920 | ||
|
|
9ec2cea95c | ||
|
|
1a4f713a79 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1 +1 @@
|
|||||||
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
[Preview this branch](https://dataexplorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
||||||
|
|||||||
103
.github/workflows/ci.yml
vendored
103
.github/workflows/ci.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
|||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build:contracts
|
- run: npm run build:contracts
|
||||||
- name: Restore Build Cache
|
- name: Restore Build Cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: .cache
|
path: .cache
|
||||||
key: ${{ runner.os }}-build-cache
|
key: ${{ runner.os }}-build-cache
|
||||||
@@ -92,18 +92,20 @@ jobs:
|
|||||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||||
- run: cp -r ./Contracts ./dist/contracts
|
- run: cp -r ./Contracts ./dist/contracts
|
||||||
- run: cp -r ./configs ./dist/configs
|
- run: cp -r ./configs ./dist/configs
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
|
- 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 cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
|
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
|
||||||
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 cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
|
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
|
||||||
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/')
|
||||||
@@ -113,21 +115,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@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
- run: cp ./configs/prod.json config.json
|
- run: cp ./configs/prod.json config.json
|
||||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text
|
||||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
|
||||||
- uses: actions/upload-artifact@v3
|
- run: dotnet nuget remove source "ADO"
|
||||||
name: packages
|
- uses: actions/upload-artifact@v4
|
||||||
|
name: Upload package to Artifacts
|
||||||
with:
|
with:
|
||||||
path: "*.nupkg"
|
name: prod-package
|
||||||
|
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/')
|
||||||
@@ -137,22 +139,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@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
- run: cp ./configs/mpac.json config.json
|
- run: cp ./configs/mpac.json config.json
|
||||||
- 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: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text
|
||||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
|
||||||
- uses: actions/upload-artifact@v3
|
- run: dotnet nuget remove source "ADO"
|
||||||
name: packages
|
- uses: actions/upload-artifact@v4
|
||||||
|
name: Upload package to Artifacts
|
||||||
with:
|
with:
|
||||||
path: "*.nupkg"
|
name: mpac-package
|
||||||
|
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 }})"
|
||||||
@@ -185,9 +186,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"
|
||||||
@@ -197,26 +198,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
|
||||||
|
|||||||
9
DataExplorer.proj
Normal file
9
DataExplorer.proj
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<NoBuild>true</NoBuild>
|
||||||
|
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||||
|
<NuspecFile>DataExplorer.nuspec</NuspecFile>
|
||||||
|
<NuspecProperties>version=$(PackageVersion)</NuspecProperties>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -18,7 +18,7 @@ Run `npm start` to start the development server and automatically rebuild on cha
|
|||||||
### Hosted Development (https://cosmos.azure.com)
|
### Hosted Development (https://cosmos.azure.com)
|
||||||
|
|
||||||
- Visit: `https://localhost:1234/hostedExplorer.html`
|
- Visit: `https://localhost:1234/hostedExplorer.html`
|
||||||
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://cdb-ms-mpac-pbe.cosmos.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||||
|
|
||||||
### Emulator Development
|
### Emulator Development
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Visit: <code>https://localhost:1234/hostedExplorer.html</code></li>
|
<li>Visit: <code>https://localhost:1234/hostedExplorer.html</code></li>
|
||||||
<li>The default webpack dev server configuration will proxy requests to the production portal backend: <code>https://main.documentdb.ext.azure.com</code>. This will allow you to use production connection strings on your local machine.</li>
|
<li>The default webpack dev server configuration will proxy requests to the production portal backend: <code>https://cdb-ms-mpac-pbe.cosmos.azure.com</code>. This will allow you to use production connection strings on your local machine.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<a href="#emulator-development" id="emulator-development" style="color: inherit; text-decoration: none;">
|
<a href="#emulator-development" id="emulator-development" style="color: inherit; text-decoration: none;">
|
||||||
<h3>Emulator Development</h3>
|
<h3>Emulator Development</h3>
|
||||||
|
|||||||
@@ -174,7 +174,11 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||||
transformIgnorePatterns: ["/node_modules/(?!@fluentui/react-icons)", "/externals/"],
|
transformIgnorePatterns: [
|
||||||
|
"/node_modules/(?!@fluentui/react-icons|(.*)/dist/browser)/",
|
||||||
|
"/node_modules/plotly.js-cartesian-dist-min",
|
||||||
|
"/externals/",
|
||||||
|
],
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||||
// unmockedModulePathPatterns: undefined,
|
// unmockedModulePathPatterns: undefined,
|
||||||
|
|||||||
@@ -61,6 +61,8 @@
|
|||||||
|
|
||||||
@GalleryBackgroundColor: #fdfdfd;
|
@GalleryBackgroundColor: #fdfdfd;
|
||||||
|
|
||||||
|
@LinkColor: #2d6da4;
|
||||||
|
|
||||||
//Icons
|
//Icons
|
||||||
@InfoIconColor: #0072c6;
|
@InfoIconColor: #0072c6;
|
||||||
@WarningIconColor: #db7500;
|
@WarningIconColor: #db7500;
|
||||||
@@ -246,6 +248,10 @@
|
|||||||
outline: 1px dashed @FocusColor;
|
outline: 1px dashed @FocusColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.focusedBorder() {
|
||||||
|
border: 1px dashed @FocusColor;
|
||||||
|
}
|
||||||
|
|
||||||
/************************************************************************************************
|
/************************************************************************************************
|
||||||
Common Toggle Switch
|
Common Toggle Switch
|
||||||
*************************************************************************************************/
|
*************************************************************************************************/
|
||||||
|
|||||||
@@ -1830,6 +1830,14 @@ 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;
|
||||||
@@ -2618,7 +2626,6 @@ a:link {
|
|||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3118,3 +3125,7 @@ a:link {
|
|||||||
background: white;
|
background: white;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebarContainer {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ a:focus {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.splashLoaderContainer {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
#divExplorer {
|
#divExplorer {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
@@ -27,26 +31,24 @@ a:focus {
|
|||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
background-color: #ffffff
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
background-color: #ffffff
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
@@ -65,17 +67,16 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
|
||||||
.nav-tabs>li>.tabNavContentContainer>.tab_Content:hover {
|
|
||||||
border-bottom: 2px solid #e0e0e0;
|
border-bottom: 2px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content,
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
|
||||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
|
||||||
border-bottom: 2px solid @FabricAccentMedium;
|
border-bottom: 2px solid @FabricAccentMedium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
||||||
border-bottom: 0px none transparent;
|
border-bottom: 0px none transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,10 +95,10 @@ a:focus {
|
|||||||
padding-bottom: @SmallSpace;
|
padding-bottom: @SmallSpace;
|
||||||
|
|
||||||
.contentWrapper {
|
.contentWrapper {
|
||||||
.statusIconContainer {
|
.statusIconContainer {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tabIconSection {
|
.tabIconSection {
|
||||||
.cancelButton {
|
.cancelButton {
|
||||||
@@ -119,7 +120,6 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.resourceTree {
|
.resourceTree {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
@@ -156,25 +156,21 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
&>.treeNodeHeader {
|
& > .treeNodeHeader {
|
||||||
background-color: @FabricAccentExtra;
|
background-color: @FabricAccentExtra;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dataExplorerErrorConsoleContainer {
|
.dataExplorerErrorConsoleContainer {
|
||||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
width: auto;
|
width: auto;
|
||||||
align-self: auto;
|
align-self: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.filterbtnstyle {
|
.filterbtnstyle {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -200,12 +196,10 @@ a:focus {
|
|||||||
border: solid 1px #d1d1d1;
|
border: solid 1px #d1d1d1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.gridRowSelected .tabdocumentsGridElement:hover {
|
.gridRowSelected .tabdocumentsGridElement:hover {
|
||||||
background-color: @FabricAccentLight !important;
|
background-color: @FabricAccentLight !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.refreshcol {
|
.refreshcol {
|
||||||
filter: brightness(0) saturate(100%);
|
filter: brightness(0) saturate(100%);
|
||||||
}
|
}
|
||||||
|
|||||||
765
package-lock.json
generated
765
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,10 +5,9 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "4.0.1-beta.3",
|
"@azure/cosmos": "4.2.0-beta.1",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "1.5.2",
|
"@azure/identity": "4.5.0",
|
||||||
"@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",
|
||||||
@@ -117,7 +116,7 @@
|
|||||||
"@babel/preset-env": "7.24.7",
|
"@babel/preset-env": "7.24.7",
|
||||||
"@babel/preset-react": "7.24.7",
|
"@babel/preset-react": "7.24.7",
|
||||||
"@babel/preset-typescript": "7.24.7",
|
"@babel/preset-typescript": "7.24.7",
|
||||||
"@playwright/test": "1.44.0",
|
"@playwright/test": "1.49.1",
|
||||||
"@testing-library/react": "11.2.3",
|
"@testing-library/react": "11.2.3",
|
||||||
"@types/applicationinsights-js": "1.0.7",
|
"@types/applicationinsights-js": "1.0.7",
|
||||||
"@types/codemirror": "0.0.56",
|
"@types/codemirror": "0.0.56",
|
||||||
@@ -170,10 +169,10 @@
|
|||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-canvas-mock": "2.5.2",
|
"jest-canvas-mock": "2.5.2",
|
||||||
"jest-circus": "29.7.0",
|
"jest-circus": "29.7.0",
|
||||||
|
"jest-environment-jsdom": "29.7.0",
|
||||||
"jest-html-loader": "1.0.0",
|
"jest-html-loader": "1.0.0",
|
||||||
"jest-react-hooks-shallow": "1.5.1",
|
"jest-react-hooks-shallow": "1.5.1",
|
||||||
"jest-trx-results-processor": "3.0.2",
|
"jest-trx-results-processor": "3.0.2",
|
||||||
"jest-environment-jsdom": "29.7.0",
|
|
||||||
"less": "3.8.1",
|
"less": "3.8.1",
|
||||||
"less-loader": "11.1.3",
|
"less-loader": "11.1.3",
|
||||||
"less-vars-loader": "1.1.0",
|
"less-vars-loader": "1.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[defaults]
|
[defaults]
|
||||||
group = stfaul
|
group = dataexplorer-preview
|
||||||
sku = P1v2
|
sku = P1V2
|
||||||
appserviceplan = stfaul_asp_Linux_centralus_0
|
appserviceplan = dataexplorer-preview
|
||||||
location = centralus
|
location = westus2
|
||||||
web = cosmos-explorer-preview
|
web = dataexplorer-preview
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ Cosmos Explorer Preview makes it possible to try a working version of any commit
|
|||||||
|
|
||||||
Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
|
Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
|
||||||
|
|
||||||
Connection string URLs: https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
|
Connection string URLs: https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
|
||||||
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
|
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
|
||||||
|
|
||||||
In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions.
|
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://cosmos-explorer-preview.azurewebsites.net/"
|
"msalRedirectURI": "https://dataexplorer-preview.azurewebsites.net/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,15 @@ const { createProxyMiddleware } = require("http-proxy-middleware");
|
|||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
const fetch = require("node-fetch");
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
const api = createProxyMiddleware("/api", {
|
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||||
target: "https://main.documentdb.ext.azure.com",
|
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
|
||||||
|
const previewStorageWebsiteEndpoint = "https://dataexplorerpreview.z5.web.core.windows.net/";
|
||||||
|
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
|
||||||
|
const githubPullRequestUrl = "https://github.com/Azure/cosmos-explorer/pull";
|
||||||
|
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
|
||||||
|
|
||||||
|
const api = createProxyMiddleware({
|
||||||
|
target: backendEndpoint,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
logLevel: "debug",
|
logLevel: "debug",
|
||||||
bypass: (req, res) => {
|
bypass: (req, res) => {
|
||||||
@@ -15,8 +22,8 @@ const api = createProxyMiddleware("/api", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const proxy = createProxyMiddleware("/proxy", {
|
const proxy = createProxyMiddleware({
|
||||||
target: "https://main.documentdb.ext.azure.com",
|
target: backendEndpoint,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
logLevel: "debug",
|
logLevel: "debug",
|
||||||
@@ -27,35 +34,38 @@ const proxy = createProxyMiddleware("/proxy", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const commit = createProxyMiddleware("/commit", {
|
const commit = createProxyMiddleware({
|
||||||
target: "https://cosmosexplorerpreview.blob.core.windows.net",
|
target: previewStorageWebsiteEndpoint,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
logLevel: "debug",
|
logLevel: "debug",
|
||||||
pathRewrite: { "^/commit": "$web/" },
|
pathRewrite: { "^/commit": "/" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(api);
|
app.use("/api", api);
|
||||||
app.use(proxy);
|
app.use("/proxy", proxy);
|
||||||
app.use(commit);
|
app.use("/commit", 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("https://api.github.com/repos/Azure/cosmos-explorer/pulls/" + pr)
|
fetch(`${githubApiUrl}/pulls/${pr}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then(({ head: { ref, sha } }) => {
|
.then(({ head: { ref, sha } }) => {
|
||||||
const prUrl = new URL("https://github.com/Azure/cosmos-explorer/pull/" + pr);
|
const prUrl = new URL(`${githubPullRequestUrl}/${pr}`);
|
||||||
prUrl.hash = ref;
|
prUrl.hash = ref;
|
||||||
search.set("feature.pr", prUrl.href);
|
search.set("feature.pr", prUrl.href);
|
||||||
|
|
||||||
const explorer = new URL("https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/explorer.html");
|
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/explorer.html`);
|
||||||
explorer.search = search.toString();
|
explorer.search = search.toString();
|
||||||
|
|
||||||
const portal = new URL("https://ms.portal.azure.com/");
|
const portal = new URL(azurePortalMpacEndpoint);
|
||||||
portal.searchParams.set("dataExplorerSource", explorer.href);
|
portal.searchParams.set("dataExplorerSource", explorer.href);
|
||||||
|
|
||||||
return res.redirect(portal.href);
|
return res.redirect(portal.href);
|
||||||
@@ -63,12 +73,10 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
|
|||||||
.catch(() => res.sendStatus(500));
|
.catch(() => res.sendStatus(500));
|
||||||
});
|
});
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
fetch("https://api.github.com/repos/Azure/cosmos-explorer/branches/master")
|
fetch(`${githubApiUrl}/branches/master`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then(({ commit: { sha } }) => {
|
.then(({ commit: { sha } }) => {
|
||||||
const explorer = new URL(
|
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/hostedExplorer.html`);
|
||||||
"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));
|
||||||
|
|||||||
1360
preview/package-lock.json
generated
1360
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "az webapp up --name \"cosmos-explorer-preview\" --subscription \"cosmosdb-portalteam-generaltest-msft\" --resource-group \"stfaul\"",
|
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2",
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
"author": "Microsoft Corporation",
|
"author": "Microsoft Corporation",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"http-proxy-middleware": "^1.1.0",
|
"http-proxy-middleware": "^3.0.3",
|
||||||
|
"node": "^18.20.6",
|
||||||
"node-fetch": "^2.6.1"
|
"node-fetch": "^2.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,6 +89,7 @@ export class CapabilityNames {
|
|||||||
public static readonly EnableMongo: string = "EnableMongo";
|
public static readonly EnableMongo: string = "EnableMongo";
|
||||||
public static readonly EnableServerless: string = "EnableServerless";
|
public static readonly EnableServerless: string = "EnableServerless";
|
||||||
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
|
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
|
||||||
|
public static readonly EnableNoSQLFullTextSearch: string = "EnableNoSQLFullTextSearch";
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CapacityMode {
|
export enum CapacityMode {
|
||||||
@@ -96,6 +97,12 @@ 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";
|
||||||
@@ -118,6 +125,7 @@ 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 {
|
||||||
@@ -148,13 +156,25 @@ export class PortalBackendEndpoints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MongoProxyEndpoints {
|
export class MongoProxyEndpoints {
|
||||||
public static readonly Local: string = "https://localhost:7238";
|
public static readonly Development: string = "https://localhost:7238";
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
||||||
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
||||||
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
||||||
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";
|
||||||
@@ -505,6 +525,18 @@ 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,4 +1,5 @@
|
|||||||
import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
import { PortalBackendEndpoints } from "Common/Constants";
|
||||||
|
import { configContext, Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient";
|
import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient";
|
||||||
|
|
||||||
@@ -20,22 +21,22 @@ describe("getTokenFromAuthService", () => {
|
|||||||
|
|
||||||
it("builds the correct URL in production", () => {
|
it("builds the correct URL in production", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
});
|
});
|
||||||
getTokenFromAuthService("GET", "dbs", "foo");
|
getTokenFromAuthService("GET", "dbs", "foo");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/guest/runtimeproxy/authorizationTokens",
|
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct URL in dev", () => {
|
it("builds the correct URL in dev", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://localhost:1234",
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development,
|
||||||
});
|
});
|
||||||
getTokenFromAuthService("GET", "dbs", "foo");
|
getTokenFromAuthService("GET", "dbs", "foo");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/guest/runtimeproxy/authorizationTokens",
|
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -78,7 +79,7 @@ describe("requestPlugin", () => {
|
|||||||
const next = jest.fn();
|
const next = jest.fn();
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
platform: Platform.Hosted,
|
platform: Platform.Hosted,
|
||||||
BACKEND_ENDPOINT: "https://localhost:1234",
|
PORTAL_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||||
PROXY_PATH: "/proxy",
|
PROXY_PATH: "/proxy",
|
||||||
});
|
});
|
||||||
const headers = {};
|
const headers = {};
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
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 } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { BackendApi, 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 { userContext } from "../UserContext";
|
import { FabricArtifactInfo, updateUserContext, 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";
|
||||||
@@ -19,7 +20,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 && userContext.apiType === "SQL";
|
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
||||||
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} `,
|
||||||
@@ -27,7 +28,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
);
|
);
|
||||||
if (!userContext.aadToken) {
|
if (!userContext.aadToken) {
|
||||||
logConsoleError(
|
logConsoleError(
|
||||||
`AAD token does not exist. Please click on "Login for Entra ID" button prior to performing Entra ID RBAC operations`,
|
`AAD token does not exist. Please use the "Login for Entra ID" button in the Toolbar prior to performing Entra ID RBAC operations`,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -42,7 +43,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configContext.platform === Platform.Fabric) {
|
if (isFabricMirroredKey()) {
|
||||||
switch (requestInfo.resourceType) {
|
switch (requestInfo.resourceType) {
|
||||||
case Cosmos.ResourceType.conflicts:
|
case Cosmos.ResourceType.conflicts:
|
||||||
case Cosmos.ResourceType.container:
|
case Cosmos.ResourceType.container:
|
||||||
@@ -54,8 +55,13 @@ 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 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
const resourceTokens = (
|
||||||
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
||||||
|
).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:
|
||||||
@@ -66,7 +72,9 @@ 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 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
const resourceTokens2 = (
|
||||||
|
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 **************
|
||||||
@@ -125,10 +133,6 @@ export async function getTokenFromAuthService(
|
|||||||
resourceType: string,
|
resourceType: string,
|
||||||
resourceId?: string,
|
resourceId?: string,
|
||||||
): Promise<AuthorizationToken> {
|
): Promise<AuthorizationToken> {
|
||||||
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
|
||||||
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||||
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
||||||
@@ -151,34 +155,6 @@ export async function getTokenFromAuthService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTokenFromAuthService_ToBeDeprecated(
|
|
||||||
verb: string,
|
|
||||||
resourceType: string,
|
|
||||||
resourceId?: string,
|
|
||||||
): Promise<AuthorizationToken> {
|
|
||||||
try {
|
|
||||||
const host = configContext.BACKEND_ENDPOINT;
|
|
||||||
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
"x-ms-encrypted-auth-token": userContext.accessToken,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
verb,
|
|
||||||
resourceType,
|
|
||||||
resourceId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
//TODO I am not sure why we have to parse the JSON again here. fetch should do it for us when we call .json()
|
|
||||||
const result = JSON.parse(await response.json());
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
||||||
enum SDKSupportedCapabilities {
|
enum SDKSupportedCapabilities {
|
||||||
None = 0,
|
None = 0,
|
||||||
@@ -189,13 +165,24 @@ let _client: Cosmos.CosmosClient;
|
|||||||
|
|
||||||
export function client(): Cosmos.CosmosClient {
|
export function client(): Cosmos.CosmosClient {
|
||||||
if (_client) {
|
if (_client) {
|
||||||
if (!userContext.hasDataPlaneRbacSettingChanged) {
|
if (!userContext.refreshCosmosClient) {
|
||||||
return _client;
|
return _client;
|
||||||
}
|
}
|
||||||
|
_client.dispose();
|
||||||
|
_client = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userContext.refreshCosmosClient) {
|
||||||
|
updateUserContext({
|
||||||
|
refreshCosmosClient: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||||
|
|
||||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||||
|
_defaultHeaders["x-ms-cosmos-throughput-bucket"] = 1;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.ConnectionString ||
|
userContext.authType === AuthType.ConnectionString ||
|
||||||
|
|||||||
34
src/Common/DatabaseAccountUtility.test.ts
Normal file
34
src/Common/DatabaseAccountUtility.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { WorkloadType } from "Common/Constants";
|
||||||
|
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||||
|
import { DatabaseAccount, Tags } from "Contracts/DataModels";
|
||||||
|
import { updateUserContext } from "UserContext";
|
||||||
|
|
||||||
|
describe("Database Account Utility", () => {
|
||||||
|
describe("Workload Type", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
tags: {} as Tags,
|
||||||
|
} as DatabaseAccount,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Workload Type should return Learning", () => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
tags: {
|
||||||
|
"hidden-workload-type": WorkloadType.Learning,
|
||||||
|
} as Tags,
|
||||||
|
} as DatabaseAccount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workloadType: WorkloadType = getWorkloadType();
|
||||||
|
expect(workloadType).toBe(WorkloadType.Learning);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Workload Type should return None", () => {
|
||||||
|
const workloadType: WorkloadType = getWorkloadType();
|
||||||
|
expect(workloadType).toBe(WorkloadType.None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { TagNames, WorkloadType } from "Common/Constants";
|
||||||
|
import { Tags } from "Contracts/DataModels";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
function isVirtualNetworkFilterEnabled() {
|
function isVirtualNetworkFilterEnabled() {
|
||||||
@@ -15,3 +17,16 @@ 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;
|
||||||
|
}
|
||||||
|
|||||||
3
src/Common/DatabaseUtility.ts
Normal file
3
src/Common/DatabaseUtility.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function getNewDatabaseSharedThroughputDefault(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ export interface TableEntityProps {
|
|||||||
isEntityValueDisable?: boolean;
|
isEntityValueDisable?: boolean;
|
||||||
entityTimeValue: string;
|
entityTimeValue: string;
|
||||||
entityValueType: string;
|
entityValueType: string;
|
||||||
|
entityProperty: string;
|
||||||
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||||
onSelectDate: (date: Date | null | undefined) => void;
|
onSelectDate: (date: Date | null | undefined) => void;
|
||||||
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||||
@@ -26,6 +27,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
|||||||
onSelectDate,
|
onSelectDate,
|
||||||
isEntityValueDisable,
|
isEntityValueDisable,
|
||||||
onEntityTimeValueChange,
|
onEntityTimeValueChange,
|
||||||
|
entityProperty,
|
||||||
}: TableEntityProps): JSX.Element => {
|
}: TableEntityProps): JSX.Element => {
|
||||||
if (isEntityTypeDate) {
|
if (isEntityTypeDate) {
|
||||||
return (
|
return (
|
||||||
@@ -51,15 +53,20 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<>
|
||||||
label={entityValueLabel && entityValueLabel}
|
<span id={entityProperty} className="screenReaderOnly">
|
||||||
className="addEntityTextField"
|
Edit Property {entityProperty} {attributeValueLabel}
|
||||||
disabled={isEntityValueDisable}
|
</span>
|
||||||
type={entityValueType}
|
<TextField
|
||||||
placeholder={entityValuePlaceholder}
|
label={entityValueLabel && entityValueLabel}
|
||||||
value={typeof entityValue === "string" ? entityValue : ""}
|
className="addEntityTextField"
|
||||||
onChange={onEntityValueChange}
|
disabled={isEntityValueDisable}
|
||||||
ariaLabel={attributeValueLabel}
|
type={entityValueType}
|
||||||
/>
|
placeholder={entityValuePlaceholder}
|
||||||
|
value={typeof entityValue === "string" ? entityValue : ""}
|
||||||
|
onChange={onEntityValueChange}
|
||||||
|
aria-labelledby={entityProperty}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
|
import { MongoProxyEndpoints } from "Common/Constants";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import {
|
import { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||||
deleteDocument,
|
|
||||||
getEndpoint,
|
|
||||||
getFeatureEndpointOrDefault,
|
|
||||||
queryDocuments,
|
|
||||||
readDocument,
|
|
||||||
updateDocument,
|
|
||||||
} from "./MongoProxyClient";
|
|
||||||
|
|
||||||
const databaseId = "testDB";
|
const databaseId = "testDB";
|
||||||
|
|
||||||
@@ -71,7 +64,8 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -82,16 +76,19 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
queryDocuments(databaseId, collection, true, "{}");
|
queryDocuments(databaseId, collection, true, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
queryDocuments(databaseId, collection, true, "{}");
|
queryDocuments(databaseId, collection, true, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -103,7 +100,8 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -114,16 +112,19 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -135,7 +136,8 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -146,16 +148,19 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
updateConfigContext({
|
||||||
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -167,7 +172,8 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -178,28 +184,20 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
updateDocument(databaseId, collection, documentId, "{}");
|
updateDocument(databaseId, collection, documentId, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
|
||||||
updateDocument(databaseId, collection, documentId, "{}");
|
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
|
||||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("deleteDocument", () => {
|
describe("deleteDocuments", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetConfigContext();
|
resetConfigContext();
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -208,18 +206,21 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
deleteDocument(databaseId, collection, documentId);
|
deleteDocuments(databaseId, collection, [documentId]);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
updateConfigContext({
|
||||||
deleteDocument(databaseId, collection, documentId);
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
|
});
|
||||||
|
deleteDocuments(databaseId, collection, [documentId]);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -231,13 +232,14 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a production endpoint", () => {
|
it("returns a production endpoint", () => {
|
||||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a development endpoint", () => {
|
it("returns a development endpoint", () => {
|
||||||
@@ -249,35 +251,8 @@ describe("MongoProxyClient", () => {
|
|||||||
updateUserContext({
|
updateUserContext({
|
||||||
authType: AuthType.EncryptedToken,
|
authType: AuthType.EncryptedToken,
|
||||||
});
|
});
|
||||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("getFeatureEndpointOrDefault", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
resetConfigContext();
|
|
||||||
updateConfigContext({
|
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
|
||||||
});
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
"feature.mongoProxyEndpoint": "https://localhost:12901",
|
|
||||||
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
|
||||||
});
|
|
||||||
const features = extractFeatures(params);
|
|
||||||
updateUserContext({
|
|
||||||
authType: AuthType.AAD,
|
|
||||||
features: features,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a local endpoint", () => {
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
|
||||||
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a production endpoint", () => {
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
|
||||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
import {
|
|
||||||
allowedMongoProxyEndpoints,
|
|
||||||
allowedMongoProxyEndpoints_ToBeDeprecated,
|
|
||||||
validateEndpoint,
|
|
||||||
} from "Utils/EndpointUtils";
|
|
||||||
import queryString from "querystring";
|
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
|
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
@@ -67,10 +60,6 @@ export function queryDocuments(
|
|||||||
query: string,
|
query: string,
|
||||||
continuationToken?: string,
|
continuationToken?: string,
|
||||||
): Promise<QueryResponse> {
|
): Promise<QueryResponse> {
|
||||||
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
|
|
||||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const params = {
|
const params = {
|
||||||
@@ -89,7 +78,7 @@ export function queryDocuments(
|
|||||||
query,
|
query,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || "";
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
@@ -127,76 +116,11 @@ export function queryDocuments(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryDocuments_ToBeDeprecated(
|
|
||||||
databaseId: string,
|
|
||||||
collection: Collection,
|
|
||||||
isResourceList: boolean,
|
|
||||||
query: string,
|
|
||||||
continuationToken?: string,
|
|
||||||
): Promise<QueryResponse> {
|
|
||||||
const { databaseAccount } = userContext;
|
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
|
||||||
const params = {
|
|
||||||
db: databaseId,
|
|
||||||
coll: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
|
||||||
rid: collection.rid,
|
|
||||||
rtype: "docs",
|
|
||||||
sid: userContext.subscriptionId,
|
|
||||||
rg: userContext.resourceGroup,
|
|
||||||
dba: databaseAccount.name,
|
|
||||||
pk:
|
|
||||||
collection && collection.partitionKey && !collection.partitionKey.systemKey
|
|
||||||
? collection.partitionKeyProperties?.[0]
|
|
||||||
: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
[CosmosSDKConstants.HttpHeaders.IsQuery]: "true",
|
|
||||||
[CosmosSDKConstants.HttpHeaders.PopulateQueryMetrics]: "true",
|
|
||||||
[CosmosSDKConstants.HttpHeaders.EnableScanInQuery]: "true",
|
|
||||||
[CosmosSDKConstants.HttpHeaders.EnableCrossPartitionQuery]: "true",
|
|
||||||
[CosmosSDKConstants.HttpHeaders.ParallelizeCrossPartitionQuery]: "true",
|
|
||||||
[HttpHeaders.contentType]: "application/query+json",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (continuationToken) {
|
|
||||||
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = isResourceList ? "/resourcelist" : "";
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(`${endpoint}${path}?${queryString.stringify(params)}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ query }),
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return {
|
|
||||||
continuationToken: response.headers.get(CosmosSDKConstants.HttpHeaders.Continuation),
|
|
||||||
documents: (await response.json()).Documents as DataModels.DocumentId[],
|
|
||||||
headers: response.headers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
await errorHandling(response, "querying documents", params);
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readDocument(
|
export function readDocument(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
if (!useMongoProxyEndpoint("readDocument")) {
|
|
||||||
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
|
||||||
}
|
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const idComponents = documentId.self.split("/");
|
const idComponents = documentId.self.split("/");
|
||||||
@@ -217,7 +141,7 @@ export function readDocument(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -237,61 +161,12 @@ export function readDocument(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readDocument_ToBeDeprecated(
|
|
||||||
databaseId: string,
|
|
||||||
collection: Collection,
|
|
||||||
documentId: DocumentId,
|
|
||||||
): Promise<DataModels.DocumentId> {
|
|
||||||
const { databaseAccount } = userContext;
|
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
|
||||||
const idComponents = documentId.self.split("/");
|
|
||||||
const path = idComponents.slice(0, 4).join("/");
|
|
||||||
const rid = encodeURIComponent(idComponents[5]);
|
|
||||||
const params = {
|
|
||||||
db: databaseId,
|
|
||||||
coll: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
|
||||||
rid,
|
|
||||||
rtype: "docs",
|
|
||||||
sid: userContext.subscriptionId,
|
|
||||||
rg: userContext.resourceGroup,
|
|
||||||
dba: databaseAccount.name,
|
|
||||||
pk:
|
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
|
||||||
? documentId.partitionKeyProperties?.[0]
|
|
||||||
: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: encodeURIComponent(
|
|
||||||
JSON.stringify(documentId.partitionKeyHeader()),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
return await errorHandling(response, "reading document", params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDocument(
|
export function createDocument(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
partitionKeyProperty: string,
|
partitionKeyProperty: string,
|
||||||
documentContent: unknown,
|
documentContent: unknown,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
if (!useMongoProxyEndpoint("createDocument")) {
|
|
||||||
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
|
||||||
}
|
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const params = {
|
const params = {
|
||||||
@@ -308,7 +183,7 @@ export function createDocument(
|
|||||||
documentContent: JSON.stringify(documentContent),
|
documentContent: JSON.stringify(documentContent),
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createDocument`, {
|
.fetch(`${endpoint}/createDocument`, {
|
||||||
@@ -328,54 +203,12 @@ export function createDocument(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDocument_ToBeDeprecated(
|
|
||||||
databaseId: string,
|
|
||||||
collection: Collection,
|
|
||||||
partitionKeyProperty: string,
|
|
||||||
documentContent: unknown,
|
|
||||||
): Promise<DataModels.DocumentId> {
|
|
||||||
const { databaseAccount } = userContext;
|
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
|
||||||
const params = {
|
|
||||||
db: databaseId,
|
|
||||||
coll: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
|
||||||
rid: collection.rid,
|
|
||||||
rtype: "docs",
|
|
||||||
sid: userContext.subscriptionId,
|
|
||||||
rg: userContext.resourceGroup,
|
|
||||||
dba: databaseAccount.name,
|
|
||||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(documentContent),
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
return await errorHandling(response, "creating document", params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateDocument(
|
export function updateDocument(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
documentContent: string,
|
documentContent: string,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
if (!useMongoProxyEndpoint("updateDocument")) {
|
|
||||||
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
|
||||||
}
|
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const idComponents = documentId.self.split("/");
|
const idComponents = documentId.self.split("/");
|
||||||
@@ -396,7 +229,7 @@ export function updateDocument(
|
|||||||
: "",
|
: "",
|
||||||
documentContent,
|
documentContent,
|
||||||
};
|
};
|
||||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -417,139 +250,6 @@ export function updateDocument(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateDocument_ToBeDeprecated(
|
|
||||||
databaseId: string,
|
|
||||||
collection: Collection,
|
|
||||||
documentId: DocumentId,
|
|
||||||
documentContent: string,
|
|
||||||
): Promise<DataModels.DocumentId> {
|
|
||||||
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 = {
|
|
||||||
db: databaseId,
|
|
||||||
coll: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
|
||||||
rid,
|
|
||||||
rtype: "docs",
|
|
||||||
sid: userContext.subscriptionId,
|
|
||||||
rg: userContext.resourceGroup,
|
|
||||||
dba: databaseAccount.name,
|
|
||||||
pk:
|
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
|
||||||
? documentId.partitionKeyProperties?.[0]
|
|
||||||
: "",
|
|
||||||
};
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
|
||||||
method: "PUT",
|
|
||||||
body: documentContent,
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
|
||||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
return await errorHandling(response, "updating document", params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
|
||||||
if (!useMongoProxyEndpoint("deleteDocument")) {
|
|
||||||
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
|
||||||
}
|
|
||||||
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("deleteDocument");
|
|
||||||
|
|
||||||
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 deleteDocument_ToBeDeprecated(
|
|
||||||
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 = {
|
|
||||||
db: databaseId,
|
|
||||||
coll: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
|
||||||
rid,
|
|
||||||
rtype: "docs",
|
|
||||||
sid: userContext.subscriptionId,
|
|
||||||
rg: userContext.resourceGroup,
|
|
||||||
dba: databaseAccount.name,
|
|
||||||
pk:
|
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
|
||||||
? documentId.partitionKeyProperties?.[0]
|
|
||||||
: "",
|
|
||||||
};
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
|
||||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.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,
|
||||||
@@ -561,7 +261,10 @@ export function deleteDocuments(
|
|||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
|
||||||
const rids = documentIds.map((documentId) => documentId.id());
|
const rids: string[] = documentIds.map((documentId) => {
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
return idComponents[5];
|
||||||
|
});
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
databaseID: databaseId,
|
databaseID: databaseId,
|
||||||
@@ -572,7 +275,7 @@ export function deleteDocuments(
|
|||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseAccountName: databaseAccount.name,
|
databaseAccountName: databaseAccount.name,
|
||||||
};
|
};
|
||||||
const endpoint = getFeatureEndpointOrDefault("bulkdelete");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/bulkdelete`, {
|
.fetch(`${endpoint}/bulkdelete`, {
|
||||||
@@ -596,9 +299,6 @@ export function deleteDocuments(
|
|||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
|
||||||
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
|
||||||
}
|
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const shardKey: string = params.partitionKey?.paths[0];
|
const shardKey: string = params.partitionKey?.paths[0];
|
||||||
|
|
||||||
@@ -619,7 +319,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
isSharded: !!shardKey,
|
isSharded: !!shardKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createCollection`, {
|
.fetch(`${endpoint}/createCollection`, {
|
||||||
@@ -639,69 +339,6 @@ export function createMongoCollectionWithProxy(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMongoCollectionWithProxy_ToBeDeprecated(
|
|
||||||
params: DataModels.CreateCollectionParams,
|
|
||||||
): Promise<DataModels.Collection> {
|
|
||||||
const { databaseAccount } = userContext;
|
|
||||||
const shardKey: string = params.partitionKey?.paths[0];
|
|
||||||
const mongoParams: DataModels.MongoParameters = {
|
|
||||||
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
|
||||||
db: params.databaseId,
|
|
||||||
coll: params.collectionId,
|
|
||||||
pk: shardKey,
|
|
||||||
offerThroughput: params.autoPilotMaxThroughput || params.offerThroughput,
|
|
||||||
cd: params.createNewDatabase,
|
|
||||||
st: params.databaseLevelThroughput,
|
|
||||||
is: !!shardKey,
|
|
||||||
rid: "",
|
|
||||||
rtype: "colls",
|
|
||||||
sid: userContext.subscriptionId,
|
|
||||||
rg: userContext.resourceGroup,
|
|
||||||
dba: databaseAccount.name,
|
|
||||||
isAutoPilot: !!params.autoPilotMaxThroughput,
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(
|
|
||||||
`${endpoint}/createCollection?${queryString.stringify(
|
|
||||||
mongoParams as unknown as queryString.ParsedUrlQueryInput,
|
|
||||||
)}`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
[HttpHeaders.contentType]: "application/json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
return await errorHandling(response, "creating collection", mongoParams);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
|
||||||
let endpoint;
|
|
||||||
if (useMongoProxyEndpoint(feature)) {
|
|
||||||
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
|
||||||
} else {
|
|
||||||
endpoint =
|
|
||||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
|
||||||
validateEndpoint(userContext.features.mongoProxyEndpoint, [
|
|
||||||
...allowedMongoProxyEndpoints,
|
|
||||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
|
||||||
])
|
|
||||||
? userContext.features.mongoProxyEndpoint
|
|
||||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_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";
|
||||||
|
|
||||||
@@ -715,21 +352,6 @@ export function getEndpoint(endpoint: string): string {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMongoProxyEndpoint(api: string): boolean {
|
|
||||||
const activeMongoProxyEndpoints: string[] = [
|
|
||||||
MongoProxyEndpoints.Local,
|
|
||||||
MongoProxyEndpoints.Mpac,
|
|
||||||
MongoProxyEndpoints.Prod,
|
|
||||||
MongoProxyEndpoints.Fairfax,
|
|
||||||
MongoProxyEndpoints.Mooncake,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
|
||||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ThrottlingError extends Error {
|
export class ThrottlingError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|||||||
@@ -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 = JSON.stringify(innerError);
|
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
|
||||||
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.
|
// Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message, along with a prefix and activity id.
|
||||||
it("handles single-nested error", () => {
|
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 = JSON.stringify(innerError);
|
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
|
||||||
const outerError = {
|
const outerError = {
|
||||||
code: "BadRequest",
|
code: "BadRequest",
|
||||||
message,
|
message,
|
||||||
@@ -91,4 +91,23 @@ describe("QueryError.tryParse", () => {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Imitate another value we've gotten from the backend, which has a doubly-nested JSON payload.
|
||||||
|
it("handles double-nested error", () => {
|
||||||
|
const outerError = {
|
||||||
|
code: "BadRequest",
|
||||||
|
message:
|
||||||
|
'{"code":"BadRequest","message":"{\\"errors\\":[{\\"severity\\":\\"Error\\",\\"location\\":{\\"start\\":7,\\"end\\":18},\\"code\\":\\"SC2005\\",\\"message\\":\\"\'nonexistent\' is not a recognized built-in function name.\\"}]}\\r\\nActivityId: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, Windows/10.0.20348 cosmos-netstandard-sdk/3.18.0"}',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
|
||||||
|
expect(result).toEqual([
|
||||||
|
new QueryError(
|
||||||
|
"'nonexistent' is not a recognized built-in function name.",
|
||||||
|
QueryErrorSeverity.Error,
|
||||||
|
"SC2005",
|
||||||
|
new QueryErrorLocation({ offset: 7, lineNumber: 7, column: 7 }, { offset: 18, lineNumber: 18, column: 18 }),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -214,16 +214,28 @@ export default class QueryError {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
// Some newer backends produce a message that contains a doubly-nested JSON payload.
|
||||||
if (message.startsWith("Message: ")) {
|
// In this case, the message we get is a fully-complete JSON object we can parse.
|
||||||
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
// So let's try that first
|
||||||
// So we use a separate variable to avoid this.
|
if (message.startsWith("{") && message.endsWith("}")) {
|
||||||
message = message.substring("Message: ".length);
|
let outer: unknown = undefined;
|
||||||
|
try {
|
||||||
|
outer = JSON.parse(message);
|
||||||
|
if (typeof outer === "object" && "message" in outer && typeof outer.message === "string") {
|
||||||
|
message = outer.message;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Just continue if the parsing fails. We'll use the fallback logic below.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = message.split("\n");
|
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 { Platform, configContext } from "../ConfigContext";
|
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||||
|
|
||||||
// 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 (configContext.platform === Platform.Fabric) {
|
if (isFabric()) {
|
||||||
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
||||||
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
||||||
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
onEntityValueChange={onEntityValueChange}
|
onEntityValueChange={onEntityValueChange}
|
||||||
onSelectDate={onSelectDate}
|
onSelectDate={onSelectDate}
|
||||||
onEntityTimeValueChange={onEntityTimeValueChange}
|
onEntityTimeValueChange={onEntityTimeValueChange}
|
||||||
|
entityProperty={entityProperty}
|
||||||
/>
|
/>
|
||||||
{!isEntityValueDisable && (
|
{!isEntityValueDisable && (
|
||||||
<TooltipHost content="Edit property" id="editTooltip">
|
<TooltipHost content="Edit property" id="editTooltip">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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";
|
||||||
@@ -24,7 +25,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
let collection: DataModels.Collection;
|
let collection: DataModels.Collection;
|
||||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
if (!isFabricNative() && 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,
|
||||||
@@ -99,6 +100,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
|||||||
if (params.vectorEmbeddingPolicy) {
|
if (params.vectorEmbeddingPolicy) {
|
||||||
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
||||||
}
|
}
|
||||||
|
if (params.fullTextPolicy) {
|
||||||
|
resource.fullTextPolicy = params.fullTextPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -270,6 +274,7 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
|
|||||||
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
|
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
|
||||||
analyticalStorageTtl: params.analyticalStorageTtl,
|
analyticalStorageTtl: params.analyticalStorageTtl,
|
||||||
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
|
||||||
|
fullTextPolicy: params.fullTextPolicy,
|
||||||
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
||||||
const collectionOptions: RequestOptions = {};
|
const collectionOptions: RequestOptions = {};
|
||||||
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
|
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
|
||||||
|
|||||||
70
src/Common/dataAccess/createMaterializedView.ts
Normal file
70
src/Common/dataAccess/createMaterializedView.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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,3 +1,4 @@
|
|||||||
|
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";
|
||||||
@@ -12,7 +13,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) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
||||||
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,6 +105,8 @@ 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,
|
||||||
@@ -114,6 +116,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
|
throughputBuckets,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +128,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
|
throughputBuckets,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ContainerResponse } from "@azure/cosmos";
|
import { ContainerResponse } from "@azure/cosmos";
|
||||||
import { Queries } from "Common/Constants";
|
import { Queries } from "Common/Constants";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||||
|
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 { userContext } from "../../UserContext";
|
import { FabricArtifactInfo, 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";
|
||||||
@@ -16,15 +17,13 @@ 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 (
|
if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
|
||||||
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 userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
for (const collectionResourceId in (
|
||||||
|
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];
|
||||||
@@ -56,7 +55,8 @@ 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,5 +126,12 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
|
|||||||
throw new Error(`Unsupported default experience type: ${apiType}`);
|
throw new Error(`Unsupported default experience type: ${apiType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
|
// TO DO: Remove when we get RP API Spec with materializedViews
|
||||||
|
/* 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 { Platform, configContext } from "ConfigContext";
|
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
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,8 +11,9 @@ 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 (configContext.platform === Platform.Fabric) {
|
if (isFabricMirroredKey() || isFabricNative()) {
|
||||||
// TODO This works, but is very slow, because it requests the token, so we skip for now
|
// For Fabric Mirroring, it is slow, because it requests the token and we don't need it.
|
||||||
|
// For Fabric Native, it is not supported.
|
||||||
console.error("Skiping readDatabaseOffer for Fabric");
|
console.error("Skiping readDatabaseOffer for Fabric");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -23,7 +24,8 @@ 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,7 +1,8 @@
|
|||||||
import { Platform, configContext } from "ConfigContext";
|
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||||
|
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 { userContext } from "../../UserContext";
|
import { FabricArtifactInfo, 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";
|
||||||
@@ -14,8 +15,13 @@ 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 (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
if (
|
||||||
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
isFabricMirroredKey() &&
|
||||||
|
(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
|
||||||
|
|
||||||
@@ -46,13 +52,28 @@ 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,4 +1,5 @@
|
|||||||
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";
|
||||||
@@ -36,7 +37,8 @@ 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,6 +1,7 @@
|
|||||||
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, UpdateOfferParams } from "../../Contracts/DataModels";
|
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import {
|
import {
|
||||||
migrateCassandraKeyspaceToAutoscale,
|
migrateCassandraKeyspaceToAutoscale,
|
||||||
@@ -56,7 +57,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) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
||||||
if (params.collectionId) {
|
if (params.collectionId) {
|
||||||
updatedOffer = await updateCollectionOfferWithARM(params);
|
updatedOffer = await updateCollectionOfferWithARM(params);
|
||||||
} else if (userContext.apiType === "Tables") {
|
} else if (userContext.apiType === "Tables") {
|
||||||
@@ -359,6 +360,13 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,15 @@ import {
|
|||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedCassandraProxyEndpoints,
|
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMongoBackendEndpoints,
|
|
||||||
allowedMongoProxyEndpoints,
|
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
|
defaultAllowedCassandraProxyEndpoints,
|
||||||
|
defaultAllowedMongoProxyEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
|
|
||||||
@@ -32,6 +31,8 @@ export interface ConfigContext {
|
|||||||
platform: Platform;
|
platform: Platform;
|
||||||
allowedArmEndpoints: ReadonlyArray<string>;
|
allowedArmEndpoints: ReadonlyArray<string>;
|
||||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||||
|
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||||
|
allowedMongoProxyEndpoints: ReadonlyArray<string>;
|
||||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
proxyPath?: string;
|
proxyPath?: string;
|
||||||
@@ -48,12 +49,9 @@ export interface ConfigContext {
|
|||||||
CATALOG_API_KEY: string;
|
CATALOG_API_KEY: string;
|
||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
BACKEND_ENDPOINT?: string;
|
|
||||||
PORTAL_BACKEND_ENDPOINT: string;
|
PORTAL_BACKEND_ENDPOINT: string;
|
||||||
NEW_BACKEND_APIS?: BackendApi[];
|
NEW_BACKEND_APIS?: BackendApi[];
|
||||||
MONGO_BACKEND_ENDPOINT?: string;
|
|
||||||
MONGO_PROXY_ENDPOINT: string;
|
MONGO_PROXY_ENDPOINT: string;
|
||||||
NEW_MONGO_APIS?: string[];
|
|
||||||
CASSANDRA_PROXY_ENDPOINT: string;
|
CASSANDRA_PROXY_ENDPOINT: string;
|
||||||
NEW_CASSANDRA_APIS?: string[];
|
NEW_CASSANDRA_APIS?: string[];
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
@@ -66,6 +64,8 @@ export interface ConfigContext {
|
|||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
msalRedirectURI?: string;
|
msalRedirectURI?: string;
|
||||||
|
globallyEnabledCassandraAPIs?: string[];
|
||||||
|
globallyEnabledMongoAPIs?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
@@ -73,9 +73,12 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||||
|
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||||
|
allowedMongoProxyEndpoints: defaultAllowedMongoProxyEndpoints,
|
||||||
allowedParentFrameOrigins: [
|
allowedParentFrameOrigins: [
|
||||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||||
|
`^https:\\/\\/cdb-(ms|ff|mc)-prod-pbe\\.cosmos\\.azure\\.(com|us|cn)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
|
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
|
||||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||||
@@ -85,7 +88,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:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net$`,
|
`^https:\\/\\/dataexplorer-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/",
|
||||||
@@ -103,24 +106,14 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
||||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
NEW_MONGO_APIS: [
|
|
||||||
"resourcelist",
|
|
||||||
"queryDocuments",
|
|
||||||
"createDocument",
|
|
||||||
"readDocument",
|
|
||||||
"updateDocument",
|
|
||||||
"deleteDocument",
|
|
||||||
"createCollectionWithProxy",
|
|
||||||
"legacyMongoShell",
|
|
||||||
// "bulkdelete",
|
|
||||||
],
|
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
|
globallyEnabledCassandraAPIs: [],
|
||||||
|
globallyEnabledMongoAPIs: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resetConfigContext(): void {
|
export function resetConfigContext(): void {
|
||||||
@@ -157,22 +150,19 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!validateEndpoint(
|
!validateEndpoint(
|
||||||
newContext.BACKEND_ENDPOINT,
|
newContext.MONGO_PROXY_ENDPOINT,
|
||||||
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
delete newContext.BACKEND_ENDPOINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
|
|
||||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
if (
|
||||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
!validateEndpoint(
|
||||||
}
|
newContext.CASSANDRA_PROXY_ENDPOINT,
|
||||||
|
configContext.allowedCassandraProxyEndpoints || defaultAllowedCassandraProxyEndpoints,
|
||||||
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
|
)
|
||||||
|
) {
|
||||||
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export enum TabKind {
|
|||||||
Graph,
|
Graph,
|
||||||
SQLQuery,
|
SQLQuery,
|
||||||
ScaleSettings,
|
ScaleSettings,
|
||||||
|
MongoQuery,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +52,8 @@ export interface OpenCollectionTab extends OpenTab {
|
|||||||
*/
|
*/
|
||||||
export interface OpenQueryTab extends OpenCollectionTab {
|
export interface OpenQueryTab extends OpenCollectionTab {
|
||||||
query: QueryInfo;
|
query: QueryInfo;
|
||||||
|
splitterDirection?: "vertical" | "horizontal";
|
||||||
|
queryViewSizePercent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 {
|
||||||
@@ -31,6 +32,7 @@ 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[];
|
||||||
@@ -159,9 +161,12 @@ 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 {
|
||||||
@@ -199,11 +204,19 @@ export interface IndexingPolicy {
|
|||||||
compositeIndexes?: any[];
|
compositeIndexes?: any[];
|
||||||
spatialIndexes?: any[];
|
spatialIndexes?: any[];
|
||||||
vectorIndexes?: VectorIndex[];
|
vectorIndexes?: VectorIndex[];
|
||||||
|
fullTextIndexes?: FullTextIndex[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorIndex {
|
export interface VectorIndex {
|
||||||
path: string;
|
path: string;
|
||||||
type: "flat" | "diskANN" | "quantizedFlat";
|
type: "flat" | "diskANN" | "quantizedFlat";
|
||||||
|
diskANNShardKey?: string;
|
||||||
|
indexingSearchListSize?: number;
|
||||||
|
quantizationByteSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullTextIndex {
|
||||||
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComputedProperty {
|
export interface ComputedProperty {
|
||||||
@@ -213,6 +226,17 @@ 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";
|
||||||
@@ -265,6 +289,12 @@ 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 {
|
||||||
@@ -329,9 +359,7 @@ export interface CreateDatabaseParams {
|
|||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCollectionParams {
|
export interface CreateCollectionParamsBase {
|
||||||
createNewDatabase: boolean;
|
|
||||||
collectionId: string;
|
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseLevelThroughput: boolean;
|
databaseLevelThroughput: boolean;
|
||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
@@ -342,6 +370,17 @@ export interface CreateCollectionParams {
|
|||||||
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 {
|
||||||
@@ -355,6 +394,16 @@ export interface VectorEmbedding {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FullTextPolicy {
|
||||||
|
defaultLanguage: string;
|
||||||
|
fullTextPaths: FullTextPath[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullTextPath {
|
||||||
|
path: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReadDatabaseOfferParams {
|
export interface ReadDatabaseOfferParams {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseResourceId?: string;
|
databaseResourceId?: string;
|
||||||
@@ -376,6 +425,7 @@ export interface UpdateOfferParams {
|
|||||||
collectionId?: string;
|
collectionId?: string;
|
||||||
migrateToAutoPilot?: boolean;
|
migrateToAutoPilot?: boolean;
|
||||||
migrateToManual?: boolean;
|
migrateToManual?: boolean;
|
||||||
|
throughputBuckets?: ThroughputBucket[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
@@ -643,3 +693,5 @@ export interface FeatureRegistration {
|
|||||||
state: string;
|
state: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Tags = { [key: string]: string };
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
export enum FabricMessageTypes {
|
export enum FabricMessageTypes {
|
||||||
GetAuthorizationToken = "GetAuthorizationToken",
|
GetAuthorizationToken = "GetAuthorizationToken",
|
||||||
GetAllResourceTokens = "GetAllResourceTokens",
|
GetAllResourceTokens = "GetAllResourceTokens",
|
||||||
|
GetAccessToken = "GetAccessToken",
|
||||||
Ready = "Ready",
|
Ready = "Ready",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,9 @@
|
|||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken } from "./FabricMessageTypes";
|
||||||
|
|
||||||
// This is the version of these messages
|
// This is the version of these messages
|
||||||
export const FABRIC_RPC_VERSION = "2";
|
export const FABRIC_RPC_VERSION = "FabricMessageV3";
|
||||||
|
|
||||||
// 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";
|
||||||
@@ -69,7 +31,7 @@ export type FabricMessageV2 =
|
|||||||
message: {
|
message: {
|
||||||
id: string;
|
id: string;
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
data: FabricDatabaseConnectionInfo | undefined;
|
data: ResourceTokenInfo | undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -79,17 +41,81 @@ export type FabricMessageV2 =
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CosmosDBTokenResponse = {
|
export type FabricMessageV3 =
|
||||||
token: string;
|
| {
|
||||||
date: string;
|
type: "newContainer";
|
||||||
};
|
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 type CosmosDBConnectionInfoResponse = {
|
export enum CosmosDbArtifactType {
|
||||||
|
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: { [resourceId: string]: string };
|
resourceTokens: Record<string, string> | undefined;
|
||||||
};
|
accessToken: string | undefined;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
credentialType: "Key" | "OAuth2" | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse {
|
||||||
resourceTokensTimestamp: number;
|
resourceTokensTimestamp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export enum MessageTypes {
|
|||||||
OpenPostgreSQLPasswordReset,
|
OpenPostgreSQLPasswordReset,
|
||||||
OpenPostgresNetworkingBlade,
|
OpenPostgresNetworkingBlade,
|
||||||
OpenCosmosDBNetworkingBlade,
|
OpenCosmosDBNetworkingBlade,
|
||||||
DisplayNPSSurvey,
|
DisplayNPSSurvey, // unused
|
||||||
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,7 +115,13 @@ export interface CollectionBase extends TreeNode {
|
|||||||
isSampleCollection?: boolean;
|
isSampleCollection?: boolean;
|
||||||
|
|
||||||
onDocumentDBDocumentsClick(): void;
|
onDocumentDBDocumentsClick(): void;
|
||||||
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
onNewQueryClick(
|
||||||
|
source: any,
|
||||||
|
event?: MouseEvent,
|
||||||
|
queryText?: string,
|
||||||
|
splitterDirection?: "horizontal" | "vertical",
|
||||||
|
queryViewSizePercent?: number,
|
||||||
|
): void;
|
||||||
expandCollection(): void;
|
expandCollection(): void;
|
||||||
collapseCollection(): void;
|
collapseCollection(): void;
|
||||||
getDatabase(): Database;
|
getDatabase(): Database;
|
||||||
@@ -126,6 +132,8 @@ export interface Collection extends CollectionBase {
|
|||||||
analyticalStorageTtl: ko.Observable<number>;
|
analyticalStorageTtl: ko.Observable<number>;
|
||||||
schema?: DataModels.ISchema;
|
schema?: DataModels.ISchema;
|
||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
|
vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
|
||||||
|
fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
|
||||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
usageSizeInKB: ko.Observable<number>;
|
usageSizeInKB: ko.Observable<number>;
|
||||||
@@ -135,6 +143,8 @@ 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[];
|
||||||
@@ -149,7 +159,13 @@ export interface Collection extends CollectionBase {
|
|||||||
onSettingsClick: () => Promise<void>;
|
onSettingsClick: () => Promise<void>;
|
||||||
|
|
||||||
onNewGraphClick(): void;
|
onNewGraphClick(): void;
|
||||||
onNewMongoQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
onNewMongoQueryClick(
|
||||||
|
source: any,
|
||||||
|
event?: MouseEvent,
|
||||||
|
queryText?: string,
|
||||||
|
splitterDirection?: "horizontal" | "vertical",
|
||||||
|
queryViewSizePercent?: number,
|
||||||
|
): void;
|
||||||
onNewMongoShellClick(): void;
|
onNewMongoShellClick(): void;
|
||||||
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
||||||
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
||||||
@@ -309,6 +325,8 @@ export interface QueryTabOptions extends TabOptions {
|
|||||||
partitionKey?: DataModels.PartitionKey;
|
partitionKey?: DataModels.PartitionKey;
|
||||||
queryText?: string;
|
queryText?: string;
|
||||||
resourceTokenPartitionKey?: string;
|
resourceTokenPartitionKey?: string;
|
||||||
|
splitterDirection?: "horizontal" | "vertical";
|
||||||
|
queryViewSizePercent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptTabOption extends TabOptions {
|
export interface ScriptTabOption extends TabOptions {
|
||||||
@@ -382,13 +400,14 @@ export interface DataExplorerInputsFrame {
|
|||||||
databaseAccount: any;
|
databaseAccount: any;
|
||||||
subscriptionId?: string;
|
subscriptionId?: string;
|
||||||
resourceGroup?: string;
|
resourceGroup?: string;
|
||||||
|
tenantId?: string;
|
||||||
|
userName?: string;
|
||||||
masterKey?: string;
|
masterKey?: string;
|
||||||
hasWriteAccess?: boolean;
|
hasWriteAccess?: boolean;
|
||||||
authorizationToken?: string;
|
authorizationToken?: string;
|
||||||
csmEndpoint?: string;
|
csmEndpoint?: string;
|
||||||
dnsSuffix?: string;
|
dnsSuffix?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
extensionEndpoint?: string;
|
|
||||||
portalBackendEndpoint?: string;
|
portalBackendEndpoint?: string;
|
||||||
mongoProxyEndpoint?: string;
|
mongoProxyEndpoint?: string;
|
||||||
cassandraProxyEndpoint?: string;
|
cassandraProxyEndpoint?: string;
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
|
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";
|
||||||
@@ -19,7 +27,6 @@ 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";
|
||||||
@@ -41,7 +48,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 (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,16 +60,18 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteDatabaseIcon,
|
iconSrc: DeleteDatabaseIcon,
|
||||||
onClick: () =>
|
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||||
useSidePanel
|
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||||
.getState()
|
useSidePanel
|
||||||
.openSidePanel(
|
.getState()
|
||||||
"Delete " + getDatabaseName(),
|
.openSidePanel(
|
||||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
"Delete " + getDatabaseName(),
|
||||||
),
|
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||||
|
);
|
||||||
|
},
|
||||||
label: `Delete ${getDatabaseName()}`,
|
label: `Delete ${getDatabaseName()}`,
|
||||||
styleClass: "deleteDatabaseMenuItem",
|
styleClass: "deleteDatabaseMenuItem",
|
||||||
});
|
});
|
||||||
@@ -143,23 +152,42 @@ export const createCollectionContextMenuButton = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configContext.platform !== Platform.Fabric) {
|
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteCollectionIcon,
|
iconSrc: DeleteCollectionIcon,
|
||||||
onClick: () => {
|
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||||
useSidePanel
|
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||||
.getState()
|
useSidePanel
|
||||||
.openSidePanel(
|
.getState()
|
||||||
"Delete " + getCollectionName(),
|
.openSidePanel(
|
||||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
"Delete " + getCollectionName(),
|
||||||
);
|
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
label: `Delete ${getCollectionName()}`,
|
label: `Delete ${getCollectionName()}`,
|
||||||
styleClass: "deleteCollectionMenuItem",
|
styleClass: "deleteCollectionMenuItem",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, Label, Stack, TooltipHost } from "@fluentui/react";
|
import { DirectionalHint, Icon, IconButton, Label, Stack, TooltipHost } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||||
@@ -9,6 +9,9 @@ export interface CollapsibleSectionProps {
|
|||||||
onExpand?: () => void;
|
onExpand?: () => void;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
tooltipContent?: string | JSX.Element | JSX.Element[];
|
tooltipContent?: string | JSX.Element | JSX.Element[];
|
||||||
|
showDelete?: boolean;
|
||||||
|
onDelete?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollapsibleSectionState {
|
export interface CollapsibleSectionState {
|
||||||
@@ -69,6 +72,20 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
)}
|
)}
|
||||||
|
{this.props.showDelete && (
|
||||||
|
<Stack.Item style={{ marginLeft: "auto" }}>
|
||||||
|
<IconButton
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
id={`delete-${this.props.title.split(" ").join("-")}`}
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ height: 27, marginRight: "20px" }}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.props.onDelete();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
{this.state.isExpanded && this.props.children}
|
{this.state.isExpanded && this.props.children}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* React component for Command button component.
|
* React component for Command button component.
|
||||||
*/
|
*/
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||||
|
import { KeyCodes } from "../../../Common/Constants";
|
||||||
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import * as StringUtils from "../../../Utils/StringUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for this component
|
* Options for this component
|
||||||
*/
|
*/
|
||||||
export interface CommandButtonComponentProps {
|
export interface CommandButtonComponentProps {
|
||||||
|
/**
|
||||||
|
* font icon name for the button
|
||||||
|
*/
|
||||||
|
iconName?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* image source for the button icon
|
* image source for the button icon
|
||||||
*/
|
*/
|
||||||
@@ -22,7 +31,7 @@ export interface CommandButtonComponentProps {
|
|||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
*/
|
*/
|
||||||
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent, container: Explorer) => void;
|
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
@@ -111,3 +120,157 @@ export interface CommandButtonComponentProps {
|
|||||||
*/
|
*/
|
||||||
keyboardAction?: KeyboardAction;
|
keyboardAction?: KeyboardAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
||||||
|
private dropdownElt: HTMLElement;
|
||||||
|
private expandButtonElt: HTMLElement;
|
||||||
|
|
||||||
|
public componentDidUpdate(): void {
|
||||||
|
if (!this.dropdownElt || !this.expandButtonElt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyPress(event: React.KeyboardEvent): boolean {
|
||||||
|
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
|
||||||
|
this.commandClickCallback && this.commandClickCallback(event);
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onLauncherKeyDown(event: React.KeyboardEvent<HTMLDivElement>): boolean {
|
||||||
|
if (event.keyCode === KeyCodes.DownArrow) {
|
||||||
|
$(this.dropdownElt).hide();
|
||||||
|
$(this.dropdownElt).show().focus();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (event.keyCode === KeyCodes.UpArrow) {
|
||||||
|
$(this.dropdownElt).hide();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCommandButtonId(): string {
|
||||||
|
if (this.props.id) {
|
||||||
|
return this.props.id;
|
||||||
|
} else {
|
||||||
|
return `commandButton-${StringUtils.stripSpacesFromString(this.props.commandButtonLabel)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static renderButton(options: CommandButtonComponentProps, key?: string): JSX.Element {
|
||||||
|
return <CommandButtonComponent key={key} {...options} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
private commandClickCallback(e: React.SyntheticEvent): void {
|
||||||
|
if (this.props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Query component's parent, not document
|
||||||
|
const el = document.querySelector(".commandDropdownContainer") as HTMLElement;
|
||||||
|
if (el) {
|
||||||
|
el.style.display = "none";
|
||||||
|
}
|
||||||
|
this.props.onCommandClick(e);
|
||||||
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
|
commandButtonClicked: this.props.commandButtonLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderChildren(): JSX.Element {
|
||||||
|
if (!this.props.children || this.props.children.length < 1) {
|
||||||
|
return <React.Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="commandExpand"
|
||||||
|
tabIndex={0}
|
||||||
|
ref={(ref: HTMLElement) => {
|
||||||
|
this.expandButtonElt = ref;
|
||||||
|
}}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => this.onLauncherKeyDown(e)}
|
||||||
|
>
|
||||||
|
<div className="commandDropdownLauncher">
|
||||||
|
<span className="partialSplitter" />
|
||||||
|
<span className="expandDropdown">
|
||||||
|
<img src={CollapseChevronDownIcon} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="commandDropdownContainer"
|
||||||
|
ref={(ref: HTMLElement) => {
|
||||||
|
this.dropdownElt = ref;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="commandDropdown">
|
||||||
|
{this.props.children.map((c: CommandButtonComponentProps, index: number): JSX.Element => {
|
||||||
|
return CommandButtonComponent.renderButton(c, `${index}`);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static renderLabel(
|
||||||
|
props: CommandButtonComponentProps,
|
||||||
|
key?: string,
|
||||||
|
refct?: (input: HTMLElement) => void,
|
||||||
|
): JSX.Element {
|
||||||
|
if (!props.commandButtonLabel) {
|
||||||
|
return <React.Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="commandLabel" key={key} ref={refct}>
|
||||||
|
{props.commandButtonLabel}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
let mainClassName = "commandButtonComponent";
|
||||||
|
if (this.props.disabled) {
|
||||||
|
mainClassName += " commandDisabled";
|
||||||
|
}
|
||||||
|
if (this.props.isSelected) {
|
||||||
|
mainClassName += " selectedButton";
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentClassName = "commandContent";
|
||||||
|
if (this.props.children && this.props.children.length > 0) {
|
||||||
|
contentClassName += " hasHiddenItems";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="commandButtonReact">
|
||||||
|
<span
|
||||||
|
className={mainClassName}
|
||||||
|
role="menuitem"
|
||||||
|
tabIndex={this.props.tabIndex}
|
||||||
|
onKeyPress={(e: React.KeyboardEvent<HTMLSpanElement>) => this.onKeyPress(e)}
|
||||||
|
title={this.props.tooltipText}
|
||||||
|
id={this.getCommandButtonId()}
|
||||||
|
aria-disabled={this.props.disabled}
|
||||||
|
aria-haspopup={this.props.hasPopup}
|
||||||
|
aria-label={this.props.ariaLabel}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLSpanElement>) => this.commandClickCallback(e)}
|
||||||
|
>
|
||||||
|
<div className={contentClassName}>
|
||||||
|
<img className="commandIcon" src={this.props.iconSrc} alt={this.props.iconAlt} />
|
||||||
|
{CommandButtonComponent.renderLabel(this.props)}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
{this.props.children && this.renderChildren()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
|
|
||||||
|
describe("AddFullTextPolicyForm", () => {
|
||||||
|
//CTODO: add tests
|
||||||
|
it.skip("should render correctly", () => {});
|
||||||
|
});
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import {
|
||||||
|
DefaultButton,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownOption,
|
||||||
|
IStyleFunctionOrObject,
|
||||||
|
ITextFieldStyleProps,
|
||||||
|
ITextFieldStyles,
|
||||||
|
Label,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import { FullTextIndex, FullTextPath, FullTextPolicy } from "Contracts/DataModels";
|
||||||
|
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface FullTextPoliciesComponentProps {
|
||||||
|
fullTextPolicy: FullTextPolicy;
|
||||||
|
onFullTextPathChange: (
|
||||||
|
fullTextPolicy: FullTextPolicy,
|
||||||
|
fullTextIndexes: FullTextIndex[],
|
||||||
|
validationPassed: boolean,
|
||||||
|
) => void;
|
||||||
|
discardChanges?: boolean;
|
||||||
|
onChangesDiscarded?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullTextPolicyData {
|
||||||
|
path: string;
|
||||||
|
language: string;
|
||||||
|
pathError: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelStyles = {
|
||||||
|
root: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||||
|
fieldGroup: {
|
||||||
|
height: 27,
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
fontSize: 12,
|
||||||
|
padding: "0 8px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdownStyles = {
|
||||||
|
title: {
|
||||||
|
height: 27,
|
||||||
|
lineHeight: "24px",
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
height: 27,
|
||||||
|
lineHeight: "24px",
|
||||||
|
},
|
||||||
|
dropdownItem: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPoliciesComponentProps> = ({
|
||||||
|
fullTextPolicy,
|
||||||
|
onFullTextPathChange,
|
||||||
|
discardChanges,
|
||||||
|
onChangesDiscarded,
|
||||||
|
}): JSX.Element => {
|
||||||
|
const getFullTextPathError = (path: string, index?: number): string => {
|
||||||
|
let error = "";
|
||||||
|
if (!path) {
|
||||||
|
error = "Full text path should not be empty";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
index >= 0 &&
|
||||||
|
fullTextPathData?.find(
|
||||||
|
(fullTextPath: FullTextPolicyData, dataIndex: number) => dataIndex !== index && fullTextPath.path === path,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
error = "Full text path is already defined";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeData = (fullTextPolicy: FullTextPolicy): FullTextPolicyData[] => {
|
||||||
|
if (!fullTextPolicy) {
|
||||||
|
fullTextPolicy = { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] };
|
||||||
|
}
|
||||||
|
return fullTextPolicy.fullTextPaths.map((fullTextPath: FullTextPath) => ({
|
||||||
|
...fullTextPath,
|
||||||
|
pathError: getFullTextPathError(fullTextPath.path),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [fullTextPathData, setFullTextPathData] = React.useState<FullTextPolicyData[]>(initializeData(fullTextPolicy));
|
||||||
|
const [defaultLanguage, setDefaultLanguage] = React.useState<string>(
|
||||||
|
fullTextPolicy ? fullTextPolicy.defaultLanguage : (getFullTextLanguageOptions()[0].key as never),
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
propagateData();
|
||||||
|
}, [fullTextPathData, defaultLanguage]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (discardChanges) {
|
||||||
|
setFullTextPathData(initializeData(fullTextPolicy));
|
||||||
|
setDefaultLanguage(fullTextPolicy.defaultLanguage);
|
||||||
|
onChangesDiscarded();
|
||||||
|
}
|
||||||
|
}, [discardChanges]);
|
||||||
|
|
||||||
|
const propagateData = () => {
|
||||||
|
const newFullTextPolicy: FullTextPolicy = {
|
||||||
|
defaultLanguage: defaultLanguage,
|
||||||
|
fullTextPaths: fullTextPathData.map((policy: FullTextPolicyData) => ({
|
||||||
|
path: policy.path,
|
||||||
|
language: policy.language,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
const fullTextIndexes: FullTextIndex[] = fullTextPathData.map((policy) => ({
|
||||||
|
path: policy.path,
|
||||||
|
}));
|
||||||
|
const validationPassed = fullTextPathData.every((policy: FullTextPolicyData) => policy.pathError === "");
|
||||||
|
onFullTextPathChange(newFullTextPolicy, fullTextIndexes, validationPassed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFullTextPathValueChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value.trim();
|
||||||
|
const fullTextPaths = [...fullTextPathData];
|
||||||
|
if (!fullTextPaths[index]?.path && !value.startsWith("/")) {
|
||||||
|
fullTextPaths[index].path = "/" + value;
|
||||||
|
} else {
|
||||||
|
fullTextPaths[index].path = value;
|
||||||
|
}
|
||||||
|
fullTextPaths[index].pathError = getFullTextPathError(value, index);
|
||||||
|
setFullTextPathData(fullTextPaths);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFullTextPathPolicyChange = (index: number, option: IDropdownOption): void => {
|
||||||
|
const policies = [...fullTextPathData];
|
||||||
|
policies[index].language = option.key as never;
|
||||||
|
setFullTextPathData(policies);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAdd = () => {
|
||||||
|
setFullTextPathData([
|
||||||
|
...fullTextPathData,
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
language: defaultLanguage,
|
||||||
|
pathError: getFullTextPathError(""),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (index: number) => {
|
||||||
|
const policies = fullTextPathData.filter((_uniqueKey, j) => index !== j);
|
||||||
|
setFullTextPathData(policies);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
|
<Stack style={{ marginBottom: 10 }}>
|
||||||
|
<Label styles={labelStyles}>Default language</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getFullTextLanguageOptions()}
|
||||||
|
selectedKey={defaultLanguage}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
setDefaultLanguage(option.key as never)
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
{fullTextPathData &&
|
||||||
|
fullTextPathData.length > 0 &&
|
||||||
|
fullTextPathData.map((fullTextPolicy: FullTextPolicyData, index: number) => (
|
||||||
|
<CollapsibleSectionComponent
|
||||||
|
key={index}
|
||||||
|
isExpandedByDefault={true}
|
||||||
|
title={`Full text path ${index + 1}`}
|
||||||
|
showDelete={true}
|
||||||
|
onDelete={() => onDelete(index)}
|
||||||
|
>
|
||||||
|
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||||
|
<Stack
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
margin: "0 0 6px 20px !important",
|
||||||
|
paddingLeft: 20,
|
||||||
|
width: "80%",
|
||||||
|
borderLeft: "1px solid",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={labelStyles}>Path</Label>
|
||||||
|
<TextField
|
||||||
|
id={`full-text-policy-path-${index + 1}`}
|
||||||
|
required={true}
|
||||||
|
placeholder="/fullTextPath1"
|
||||||
|
styles={textFieldStyles}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onFullTextPathValueChange(index, event)}
|
||||||
|
value={fullTextPolicy.path || ""}
|
||||||
|
errorMessage={fullTextPolicy.pathError}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={labelStyles}>Language</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getFullTextLanguageOptions()}
|
||||||
|
selectedKey={fullTextPolicy.language}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onFullTextPathPolicyChange(index, option)
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
))}
|
||||||
|
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
|
||||||
|
Add full text path
|
||||||
|
</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFullTextLanguageOptions = (): IDropdownOption[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "en-US",
|
||||||
|
text: "English (US)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
314
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
314
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
// This component is used to create a dropdown list of options for the user to select from.
|
||||||
|
// The options are displayed in a dropdown list when the user clicks on the input field.
|
||||||
|
// The user can then select an option from the list. The selected option is then displayed in the input field.
|
||||||
|
|
||||||
|
import { getTheme } from "@fluentui/react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
Link,
|
||||||
|
makeStyles,
|
||||||
|
Popover,
|
||||||
|
PopoverProps,
|
||||||
|
PopoverSurface,
|
||||||
|
PositioningImperativeRef,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { ArrowDownRegular, DismissRegular } from "@fluentui/react-icons";
|
||||||
|
import { NormalizedEventKey } from "Common/Constants";
|
||||||
|
import { tokens } from "Explorer/Theme/ThemeUtil";
|
||||||
|
import React, { FC, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
container: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flexGrow: 1,
|
||||||
|
paddingRight: 0,
|
||||||
|
outline: "none",
|
||||||
|
"& input:focus": {
|
||||||
|
outline: "none", // Undo body :focus dashed outline
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputButton: {
|
||||||
|
border: 0,
|
||||||
|
},
|
||||||
|
dropdownHeader: {
|
||||||
|
width: "100%",
|
||||||
|
fontSize: tokens.fontSizeBase300,
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: `${tokens.spacingVerticalM} 0 0 ${tokens.spacingVerticalM}`,
|
||||||
|
},
|
||||||
|
dropdownStack: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: tokens.spacingVerticalS,
|
||||||
|
marginTop: tokens.spacingVerticalS,
|
||||||
|
marginBottom: "1px",
|
||||||
|
},
|
||||||
|
dropdownOption: {
|
||||||
|
fontSize: tokens.fontSizeBase300,
|
||||||
|
fontWeight: 400,
|
||||||
|
justifyContent: "left",
|
||||||
|
padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`,
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
border: 0,
|
||||||
|
":hover": {
|
||||||
|
outline: `1px dashed ${tokens.colorNeutralForeground1Hover}`,
|
||||||
|
backgroundColor: tokens.colorNeutralBackground2Hover,
|
||||||
|
color: tokens.colorNeutralForeground1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomSection: {
|
||||||
|
fontSize: tokens.fontSizeBase300,
|
||||||
|
fontWeight: 400,
|
||||||
|
padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`,
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface InputDatalistDropdownOptionSection {
|
||||||
|
label: string;
|
||||||
|
options: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputDataListProps {
|
||||||
|
dropdownOptions: InputDatalistDropdownOptionSection[];
|
||||||
|
placeholder?: string;
|
||||||
|
title?: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
|
autofocus?: boolean; // true: acquire focus on first render
|
||||||
|
bottomLink?: {
|
||||||
|
text: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputDataList: FC<InputDataListProps> = ({
|
||||||
|
dropdownOptions,
|
||||||
|
placeholder,
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onKeyDown,
|
||||||
|
autofocus,
|
||||||
|
bottomLink,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const [showDropdown, setShowDropdown] = React.useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const positioningRef = React.useRef<PositioningImperativeRef>(null);
|
||||||
|
const [isInputFocused, setIsInputFocused] = React.useState(autofocus);
|
||||||
|
const [autofocusFirstDropdownItem, setAutofocusFirstDropdownItem] = React.useState(false);
|
||||||
|
|
||||||
|
const theme = getTheme();
|
||||||
|
const itemRefs = useRef([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
positioningRef.current?.setTarget(inputRef.current);
|
||||||
|
}
|
||||||
|
}, [inputRef, positioningRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInputFocused) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [isInputFocused]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autofocusFirstDropdownItem && showDropdown) {
|
||||||
|
// Autofocus on first item if input isn't focused
|
||||||
|
itemRefs.current[0]?.focus();
|
||||||
|
setAutofocusFirstDropdownItem(false);
|
||||||
|
}
|
||||||
|
}, [autofocusFirstDropdownItem, showDropdown]);
|
||||||
|
|
||||||
|
const handleOpenChange: PopoverProps["onOpenChange"] = (e, data) => {
|
||||||
|
if (isInputFocused && !data.open) {
|
||||||
|
// Don't close if input is focused and we're opening the dropdown (which will steal the focus)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowDropdown(data.open || false);
|
||||||
|
if (data.open) {
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === NormalizedEventKey.Escape) {
|
||||||
|
setShowDropdown(false);
|
||||||
|
} else if (e.key === NormalizedEventKey.DownArrow) {
|
||||||
|
setShowDropdown(true);
|
||||||
|
setAutofocusFirstDropdownItem(true);
|
||||||
|
}
|
||||||
|
onKeyDown(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownDropdownItemKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLButtonElement | HTMLAnchorElement>,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
if (e.key === NormalizedEventKey.Enter) {
|
||||||
|
e.currentTarget.click();
|
||||||
|
} else if (e.key === NormalizedEventKey.Escape) {
|
||||||
|
setShowDropdown(false);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
} else if (e.key === NormalizedEventKey.DownArrow) {
|
||||||
|
if (index + 1 < itemRefs.current.length) {
|
||||||
|
itemRefs.current[index + 1].focus();
|
||||||
|
} else {
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}
|
||||||
|
} else if (e.key === NormalizedEventKey.UpArrow) {
|
||||||
|
if (index - 1 >= 0) {
|
||||||
|
itemRefs.current[index - 1].focus();
|
||||||
|
} else {
|
||||||
|
// Last item, focus back to input
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flatten dropdownOptions to better manage refs and focus
|
||||||
|
let flatIndex = 0;
|
||||||
|
const indexMap = new Map<string, number>();
|
||||||
|
for (let sectionIndex = 0; sectionIndex < dropdownOptions.length; sectionIndex++) {
|
||||||
|
const section = dropdownOptions[sectionIndex];
|
||||||
|
for (let optionIndex = 0; optionIndex < section.options.length; optionIndex++) {
|
||||||
|
indexMap.set(`${sectionIndex}-${optionIndex}`, flatIndex);
|
||||||
|
flatIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
id="filterInput"
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
autoComplete="off"
|
||||||
|
className={`filterInput ${styles.input}`}
|
||||||
|
title={title}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
// Don't show dropdown if there is already a value in the input field (when user is typing)
|
||||||
|
setShowDropdown(!(newValue.length > 0));
|
||||||
|
onChange(newValue);
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
// Don't show dropdown if there is already a value in the input field
|
||||||
|
// or isInputFocused is undefined which means component is mounting
|
||||||
|
setShowDropdown(!(value.length > 0) && isInputFocused !== undefined);
|
||||||
|
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setIsInputFocused(false);
|
||||||
|
}}
|
||||||
|
contentAfter={
|
||||||
|
value.length > 0 ? (
|
||||||
|
<Button
|
||||||
|
aria-label="Clear filter"
|
||||||
|
className={styles.inputButton}
|
||||||
|
size="small"
|
||||||
|
icon={<DismissRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
onChange("");
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
aria-label="Open dropdown"
|
||||||
|
className={styles.inputButton}
|
||||||
|
size="small"
|
||||||
|
icon={<ArrowDownRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
setShowDropdown(true);
|
||||||
|
setAutofocusFirstDropdownItem(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Popover
|
||||||
|
inline
|
||||||
|
unstable_disableAutoFocus
|
||||||
|
// trapFocus
|
||||||
|
open={showDropdown}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
positioning={{ positioningRef, position: "below", align: "start", offset: 4 }}
|
||||||
|
>
|
||||||
|
<PopoverSurface className={styles.container}>
|
||||||
|
{dropdownOptions.map((section, sectionIndex) => (
|
||||||
|
<div key={section.label}>
|
||||||
|
<div className={styles.dropdownHeader} style={{ color: theme.palette.themePrimary }}>
|
||||||
|
{section.label}
|
||||||
|
</div>
|
||||||
|
<div className={styles.dropdownStack}>
|
||||||
|
{section.options.map((option, index) => (
|
||||||
|
<Button
|
||||||
|
key={option}
|
||||||
|
ref={(el) => (itemRefs.current[indexMap.get(`${sectionIndex}-${index}`)] = el)}
|
||||||
|
appearance="transparent"
|
||||||
|
shape="square"
|
||||||
|
className={styles.dropdownOption}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(option);
|
||||||
|
setShowDropdown(false);
|
||||||
|
setIsInputFocused(true);
|
||||||
|
}}
|
||||||
|
onBlur={() =>
|
||||||
|
!bottomLink &&
|
||||||
|
sectionIndex === dropdownOptions.length - 1 &&
|
||||||
|
index === section.options.length - 1 &&
|
||||||
|
setShowDropdown(false)
|
||||||
|
}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) =>
|
||||||
|
handleDownDropdownItemKeyDown(e, indexMap.get(`${sectionIndex}-${index}`))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{bottomLink && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<div className={styles.bottomSection}>
|
||||||
|
<Link
|
||||||
|
ref={(el) => (itemRefs.current[flatIndex] = el)}
|
||||||
|
href={bottomLink.url}
|
||||||
|
target="_blank"
|
||||||
|
onBlur={() => setShowDropdown(false)}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent<HTMLAnchorElement>) => handleDownDropdownItemKeyDown(e, flatIndex)}
|
||||||
|
>
|
||||||
|
{bottomLink.text}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PopoverSurface>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
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";
|
||||||
@@ -247,4 +249,42 @@ 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,12 +4,15 @@ import {
|
|||||||
ComputedPropertiesComponentProps,
|
ComputedPropertiesComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
||||||
import {
|
import {
|
||||||
ContainerVectorPolicyComponent,
|
ContainerPolicyComponent,
|
||||||
ContainerVectorPolicyComponentProps,
|
ContainerPolicyComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import {
|
||||||
|
ThroughputBucketsComponent,
|
||||||
|
ThroughputBucketsComponentProps,
|
||||||
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
@@ -33,6 +36,7 @@ import {
|
|||||||
PartitionKeyComponent,
|
PartitionKeyComponent,
|
||||||
PartitionKeyComponentProps,
|
PartitionKeyComponentProps,
|
||||||
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
||||||
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import "./SettingsComponent.less";
|
import "./SettingsComponent.less";
|
||||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||||
@@ -41,6 +45,10 @@ 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,
|
||||||
@@ -86,6 +94,8 @@ 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;
|
||||||
@@ -104,6 +114,14 @@ 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;
|
||||||
@@ -148,8 +166,11 @@ 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) {
|
||||||
@@ -163,9 +184,16 @@ 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 =
|
||||||
@@ -184,6 +212,8 @@ 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,
|
||||||
@@ -202,6 +232,14 @@ 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,
|
||||||
@@ -307,10 +345,12 @@ 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
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -318,10 +358,12 @@ 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
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -401,10 +443,14 @@ 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: [],
|
||||||
@@ -416,11 +462,14 @@ 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,
|
||||||
@@ -448,9 +497,21 @@ 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 => {
|
||||||
@@ -538,6 +599,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
|
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
|
||||||
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
|
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
|
||||||
|
|
||||||
|
private onVectorEmbeddingPolicyDirtyChange = (isVectorEmbeddingPolicyDirty: boolean): void =>
|
||||||
|
this.setState({ isContainerPolicyDirty: isVectorEmbeddingPolicyDirty });
|
||||||
|
|
||||||
|
private onFullTextPolicyDirtyChange = (isFullTextPolicyDirty: boolean): void =>
|
||||||
|
this.setState({ isContainerPolicyDirty: isFullTextPolicyDirty });
|
||||||
|
|
||||||
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
|
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
|
||||||
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
|
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
|
||||||
|
|
||||||
@@ -691,6 +758,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||||
? ChangeFeedPolicyState.On
|
? ChangeFeedPolicyState.On
|
||||||
: ChangeFeedPolicyState.Off;
|
: ChangeFeedPolicyState.Off;
|
||||||
|
const vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy =
|
||||||
|
this.collection.vectorEmbeddingPolicy && this.collection.vectorEmbeddingPolicy();
|
||||||
|
const fullTextPolicy: DataModels.FullTextPolicy =
|
||||||
|
this.collection.fullTextPolicy && this.collection.fullTextPolicy();
|
||||||
const indexingPolicyContent = this.collection.indexingPolicy();
|
const indexingPolicyContent = this.collection.indexingPolicy();
|
||||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||||
@@ -709,9 +780,13 @@ 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,
|
||||||
@@ -724,6 +799,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
|
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
|
||||||
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
|
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
|
||||||
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
|
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
|
||||||
|
vectorEmbeddingPolicy: vectorEmbeddingPolicy,
|
||||||
|
vectorEmbeddingPolicyBaseline: vectorEmbeddingPolicy,
|
||||||
|
fullTextPolicy: fullTextPolicy,
|
||||||
|
fullTextPolicyBaseline: fullTextPolicy,
|
||||||
indexingPolicyContent: indexingPolicyContent,
|
indexingPolicyContent: indexingPolicyContent,
|
||||||
indexingPolicyContentBaseline: indexingPolicyContent,
|
indexingPolicyContentBaseline: indexingPolicyContent,
|
||||||
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
|
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
|
||||||
@@ -795,6 +874,10 @@ 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 });
|
||||||
|
|
||||||
@@ -854,6 +937,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
|
this.state.isContainerPolicyDirty ||
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty
|
this.state.isComputedPropertiesDirty
|
||||||
@@ -875,6 +959,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
||||||
newCollection.defaultTtl = defaultTtl;
|
newCollection.defaultTtl = defaultTtl;
|
||||||
|
|
||||||
|
newCollection.vectorEmbeddingPolicy = this.state.vectorEmbeddingPolicy;
|
||||||
|
|
||||||
|
newCollection.fullTextPolicy = this.state.fullTextPolicy;
|
||||||
|
|
||||||
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
||||||
|
|
||||||
newCollection.changeFeedPolicy =
|
newCollection.changeFeedPolicy =
|
||||||
@@ -913,6 +1001,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||||
this.collection.computedProperties(updatedCollection.computedProperties);
|
this.collection.computedProperties(updatedCollection.computedProperties);
|
||||||
|
this.collection.vectorEmbeddingPolicy(updatedCollection.vectorEmbeddingPolicy);
|
||||||
|
this.collection.fullTextPolicy(updatedCollection.fullTextPolicy);
|
||||||
|
|
||||||
if (wasIndexingPolicyModified) {
|
if (wasIndexingPolicyModified) {
|
||||||
await this.refreshIndexTransformationProgress();
|
await this.refreshIndexTransformationProgress();
|
||||||
@@ -921,6 +1011,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.setState({
|
this.setState({
|
||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
|
isContainerPolicyDirty: false,
|
||||||
isIndexingPolicyDirty: false,
|
isIndexingPolicyDirty: false,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
isComputedPropertiesDirty: false,
|
isComputedPropertiesDirty: false,
|
||||||
@@ -977,6 +1068,24 @@ 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,
|
||||||
@@ -1091,6 +1200,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange,
|
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const containerPolicyComponentProps: ContainerPolicyComponentProps = {
|
||||||
|
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicy,
|
||||||
|
vectorEmbeddingPolicyBaseline: this.state.vectorEmbeddingPolicyBaseline,
|
||||||
|
onVectorEmbeddingPolicyChange: this.onVectorEmbeddingPolicyChange,
|
||||||
|
onVectorEmbeddingPolicyDirtyChange: this.onVectorEmbeddingPolicyDirtyChange,
|
||||||
|
isVectorSearchEnabled: this.isVectorSearchEnabled,
|
||||||
|
fullTextPolicy: this.state.fullTextPolicy,
|
||||||
|
fullTextPolicyBaseline: this.state.fullTextPolicyBaseline,
|
||||||
|
onFullTextPolicyChange: this.onFullTextPolicyChange,
|
||||||
|
onFullTextPolicyDirtyChange: this.onFullTextPolicyDirtyChange,
|
||||||
|
isFullTextSearchEnabled: this.isFullTextSearchEnabled,
|
||||||
|
shouldDiscardContainerPolicies: this.state.shouldDiscardContainerPolicies,
|
||||||
|
resetShouldDiscardContainerPolicyChange: this.resetShouldDiscardContainerPolicies,
|
||||||
|
};
|
||||||
|
|
||||||
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
||||||
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
||||||
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
||||||
@@ -1142,14 +1266,22 @@ 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 containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
|
const materializedViewComponentProps: MaterializedViewComponentProps = {
|
||||||
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
|
collection: this.collection,
|
||||||
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
@@ -1165,10 +1297,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isVectorSearchEnabled) {
|
if (this.isVectorSearchEnabled || this.isFullTextSearchEnabled) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
||||||
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
|
content: <ContainerPolicyComponent {...containerPolicyComponentProps} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1208,6 +1340,20 @@ 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],
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
|
|
||||||
|
describe("ContainerPolicyComponent", () => {
|
||||||
|
//CTODO: add tests
|
||||||
|
it.skip("should render correctly", () => {});
|
||||||
|
});
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { DefaultButton, Pivot, PivotItem, Stack } from "@fluentui/react";
|
||||||
|
import { FullTextPolicy, VectorEmbedding, VectorEmbeddingPolicy } from "Contracts/DataModels";
|
||||||
|
import {
|
||||||
|
FullTextPoliciesComponent,
|
||||||
|
getFullTextLanguageOptions,
|
||||||
|
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||||
|
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
||||||
|
import { ContainerPolicyTabTypes, isDirty } from "Explorer/Controls/Settings/SettingsUtils";
|
||||||
|
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface ContainerPolicyComponentProps {
|
||||||
|
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
|
||||||
|
vectorEmbeddingPolicyBaseline: VectorEmbeddingPolicy;
|
||||||
|
onVectorEmbeddingPolicyChange: (newVectorEmbeddingPolicy: VectorEmbeddingPolicy) => void;
|
||||||
|
onVectorEmbeddingPolicyDirtyChange: (isVectorEmbeddingPolicyDirty: boolean) => void;
|
||||||
|
isVectorSearchEnabled: boolean;
|
||||||
|
fullTextPolicy: FullTextPolicy;
|
||||||
|
fullTextPolicyBaseline: FullTextPolicy;
|
||||||
|
onFullTextPolicyChange: (newFullTextPolicy: FullTextPolicy) => void;
|
||||||
|
onFullTextPolicyDirtyChange: (isFullTextPolicyDirty: boolean) => void;
|
||||||
|
isFullTextSearchEnabled: boolean;
|
||||||
|
shouldDiscardContainerPolicies: boolean;
|
||||||
|
resetShouldDiscardContainerPolicyChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> = ({
|
||||||
|
vectorEmbeddingPolicy,
|
||||||
|
vectorEmbeddingPolicyBaseline,
|
||||||
|
onVectorEmbeddingPolicyChange,
|
||||||
|
onVectorEmbeddingPolicyDirtyChange,
|
||||||
|
isVectorSearchEnabled,
|
||||||
|
fullTextPolicy,
|
||||||
|
fullTextPolicyBaseline,
|
||||||
|
onFullTextPolicyChange,
|
||||||
|
onFullTextPolicyDirtyChange,
|
||||||
|
isFullTextSearchEnabled,
|
||||||
|
shouldDiscardContainerPolicies,
|
||||||
|
resetShouldDiscardContainerPolicyChange,
|
||||||
|
}) => {
|
||||||
|
const [selectedTab, setSelectedTab] = React.useState<ContainerPolicyTabTypes>(
|
||||||
|
ContainerPolicyTabTypes.VectorPolicyTab,
|
||||||
|
);
|
||||||
|
const [vectorEmbeddings, setVectorEmbeddings] = React.useState<VectorEmbedding[]>();
|
||||||
|
const [vectorEmbeddingsBaseline, setVectorEmbeddingsBaseline] = React.useState<VectorEmbedding[]>();
|
||||||
|
const [discardVectorChanges, setDiscardVectorChanges] = React.useState<boolean>(false);
|
||||||
|
const [fullTextSearchPolicy, setFullTextSearchPolicy] = React.useState<FullTextPolicy>();
|
||||||
|
const [fullTextSearchPolicyBaseline, setFullTextSearchPolicyBaseline] = React.useState<FullTextPolicy>();
|
||||||
|
const [discardFullTextChanges, setDiscardFullTextChanges] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setVectorEmbeddings(vectorEmbeddingPolicy?.vectorEmbeddings);
|
||||||
|
setVectorEmbeddingsBaseline(vectorEmbeddingPolicyBaseline?.vectorEmbeddings);
|
||||||
|
}, [vectorEmbeddingPolicy]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setFullTextSearchPolicy(fullTextPolicy);
|
||||||
|
setFullTextSearchPolicyBaseline(fullTextPolicyBaseline);
|
||||||
|
}, [fullTextPolicy, fullTextPolicyBaseline]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (shouldDiscardContainerPolicies) {
|
||||||
|
setVectorEmbeddings(vectorEmbeddingPolicyBaseline?.vectorEmbeddings);
|
||||||
|
setDiscardVectorChanges(true);
|
||||||
|
setFullTextSearchPolicy(fullTextPolicyBaseline);
|
||||||
|
setDiscardFullTextChanges(true);
|
||||||
|
resetShouldDiscardContainerPolicyChange();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkAndSendVectorEmbeddingPoliciesToSettings = (newVectorEmbeddings: VectorEmbedding[]): void => {
|
||||||
|
if (isDirty(newVectorEmbeddings, vectorEmbeddingsBaseline)) {
|
||||||
|
onVectorEmbeddingPolicyDirtyChange(true);
|
||||||
|
onVectorEmbeddingPolicyChange({ vectorEmbeddings: newVectorEmbeddings });
|
||||||
|
} else {
|
||||||
|
resetShouldDiscardContainerPolicyChange();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkAndSendFullTextPolicyToSettings = (newFullTextPolicy: FullTextPolicy): void => {
|
||||||
|
if (isDirty(newFullTextPolicy, fullTextSearchPolicyBaseline)) {
|
||||||
|
onFullTextPolicyDirtyChange(true);
|
||||||
|
onFullTextPolicyChange(newFullTextPolicy);
|
||||||
|
} else {
|
||||||
|
resetShouldDiscardContainerPolicyChange();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorChangesDiscarded = (): void => {
|
||||||
|
setDiscardVectorChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFullTextChangesDiscarded = (): void => {
|
||||||
|
setDiscardFullTextChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPivotChange = (item: PivotItem): void => {
|
||||||
|
const selectedTab = ContainerPolicyTabTypes[item.props.itemKey as keyof typeof ContainerPolicyTabTypes];
|
||||||
|
setSelectedTab(selectedTab);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Pivot onLinkClick={onPivotChange} selectedKey={ContainerPolicyTabTypes[selectedTab]}>
|
||||||
|
{isVectorSearchEnabled && (
|
||||||
|
<PivotItem
|
||||||
|
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.VectorPolicyTab]}
|
||||||
|
style={{ marginTop: 20 }}
|
||||||
|
headerText="Vector Policy"
|
||||||
|
>
|
||||||
|
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
|
||||||
|
{vectorEmbeddings && (
|
||||||
|
<VectorEmbeddingPoliciesComponent
|
||||||
|
disabled={true}
|
||||||
|
vectorEmbeddings={vectorEmbeddings}
|
||||||
|
vectorIndexes={undefined}
|
||||||
|
onVectorEmbeddingChange={(vectorEmbeddings: VectorEmbedding[]) =>
|
||||||
|
checkAndSendVectorEmbeddingPoliciesToSettings(vectorEmbeddings)
|
||||||
|
}
|
||||||
|
discardChanges={discardVectorChanges}
|
||||||
|
onChangesDiscarded={onVectorChangesDiscarded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</PivotItem>
|
||||||
|
)}
|
||||||
|
{isFullTextSearchEnabled && (
|
||||||
|
<PivotItem
|
||||||
|
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.FullTextPolicyTab]}
|
||||||
|
style={{ marginTop: 20 }}
|
||||||
|
headerText="Full Text Policy"
|
||||||
|
>
|
||||||
|
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
|
||||||
|
{fullTextSearchPolicy ? (
|
||||||
|
<FullTextPoliciesComponent
|
||||||
|
fullTextPolicy={fullTextSearchPolicy}
|
||||||
|
onFullTextPathChange={(newFullTextPolicy: FullTextPolicy) =>
|
||||||
|
checkAndSendFullTextPolicyToSettings(newFullTextPolicy)
|
||||||
|
}
|
||||||
|
discardChanges={discardFullTextChanges}
|
||||||
|
onChangesDiscarded={onFullTextChangesDiscarded}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DefaultButton
|
||||||
|
id={"create-full-text-policy"}
|
||||||
|
styles={{ root: { fontSize: 12 } }}
|
||||||
|
onClick={() => {
|
||||||
|
checkAndSendFullTextPolicyToSettings({
|
||||||
|
defaultLanguage: getFullTextLanguageOptions()[0].key as never,
|
||||||
|
fullTextPaths: [],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create new full text search policy
|
||||||
|
</DefaultButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</PivotItem>
|
||||||
|
)}
|
||||||
|
</Pivot>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Stack } from "@fluentui/react";
|
|
||||||
import { VectorEmbeddingPolicy } from "Contracts/DataModels";
|
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
|
||||||
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface ContainerVectorPolicyComponentProps {
|
|
||||||
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContainerVectorPolicyComponent: React.FC<ContainerVectorPolicyComponentProps> = ({
|
|
||||||
vectorEmbeddingPolicy,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative" } }}>
|
|
||||||
<EditorReact
|
|
||||||
language={"json"}
|
|
||||||
content={JSON.stringify(vectorEmbeddingPolicy || {}, null, 4)}
|
|
||||||
isReadOnly={true}
|
|
||||||
wordWrap={"on"}
|
|
||||||
ariaLabel={"Container vector policy"}
|
|
||||||
lineNumbers={"on"}
|
|
||||||
scrollBeyondLastLine={false}
|
|
||||||
className={"settingsV2Editor"}
|
|
||||||
spinnerClassName={"settingsV2EditorSpinner"}
|
|
||||||
fontSize={14}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -120,11 +120,6 @@ export class IndexingPolicyComponent extends React.Component<
|
|||||||
indexTransformationProgress={this.props.indexTransformationProgress}
|
indexTransformationProgress={this.props.indexTransformationProgress}
|
||||||
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
||||||
/>
|
/>
|
||||||
{this.props.isVectorSearchEnabled && (
|
|
||||||
<MessageBar messageBarType={MessageBarType.severeWarning}>
|
|
||||||
Container vector policies and vector indexes are not modifiable after container creation
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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,6 +14,7 @@ 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 {
|
||||||
@@ -177,12 +178,14 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
|||||||
To change the partition key, a new destination container must be created or an existing destination container
|
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>
|
||||||
<PrimaryButton
|
{configContext.platform !== Platform.Emulator && (
|
||||||
styles={{ root: { width: "fit-content" } }}
|
<PrimaryButton
|
||||||
text="Change"
|
styles={{ root: { width: "fit-content" } }}
|
||||||
onClick={startPartitionkeyChangeWorkflow}
|
text="Change"
|
||||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
onClick={startPartitionkeyChangeWorkflow}
|
||||||
/>
|
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{portalDataTransferJob && (
|
{portalDataTransferJob && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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,14 +17,13 @@ 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, usageInGB } from "../../../../../Utils/PricingUtils";
|
import { calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
||||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||||
import {
|
import {
|
||||||
PriceBreakdown,
|
PriceBreakdown,
|
||||||
@@ -366,29 +365,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private minRUperGBSurvey = (): JSX.Element => {
|
|
||||||
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
|
|
||||||
const oneTBinKB = 1000000000;
|
|
||||||
const minRUperGB = 10;
|
|
||||||
const featureFlagEnabled = userContext.features.showMinRUSurvey;
|
|
||||||
const collectionIsEligible =
|
|
||||||
userContext.subscriptionType !== SubscriptionType.Internal &&
|
|
||||||
this.props.usageSizeInKB > oneTBinKB &&
|
|
||||||
this.props.minimum >= usageInGB(this.props.usageSizeInKB) * minRUperGB;
|
|
||||||
if (featureFlagEnabled || collectionIsEligible) {
|
|
||||||
return (
|
|
||||||
<Text>
|
|
||||||
Need to scale below {this.props.minimum} RU/s? Reach out by filling{" "}
|
|
||||||
<a target="_blank" rel="noreferrer" href={href}>
|
|
||||||
this questionnaire
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderThroughputModeChoices = (): JSX.Element => {
|
private renderThroughputModeChoices = (): JSX.Element => {
|
||||||
const labelId = "settingsV2RadioButtonLabelId";
|
const labelId = "settingsV2RadioButtonLabelId";
|
||||||
return (
|
return (
|
||||||
@@ -661,7 +637,6 @@ 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,7 +4,15 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||||
|
|
||||||
const zeroValue = 0;
|
const zeroValue = 0;
|
||||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
|
export type isDirtyTypes =
|
||||||
|
| boolean
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| DataModels.IndexingPolicy
|
||||||
|
| DataModels.ComputedProperties
|
||||||
|
| DataModels.VectorEmbedding[]
|
||||||
|
| DataModels.FullTextPolicy
|
||||||
|
| 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";
|
||||||
@@ -48,6 +56,13 @@ export enum SettingsV2TabTypes {
|
|||||||
PartitionKeyTab,
|
PartitionKeyTab,
|
||||||
ComputedPropertiesTab,
|
ComputedPropertiesTab,
|
||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
|
ThroughputBucketsTab,
|
||||||
|
MaterializedViewTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ContainerPolicyTabTypes {
|
||||||
|
VectorPolicyTab,
|
||||||
|
FullTextPolicyTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsComponentDirtyResult {
|
export interface IsComponentDirtyResult {
|
||||||
@@ -154,7 +169,11 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
|||||||
case SettingsV2TabTypes.ComputedPropertiesTab:
|
case SettingsV2TabTypes.ComputedPropertiesTab:
|
||||||
return "Computed Properties";
|
return "Computed Properties";
|
||||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||||
return "Container Vector Policy (preview)";
|
return "Container Policies";
|
||||||
|
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,6 +46,17 @@ 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,10 +55,13 @@ 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",
|
||||||
@@ -71,6 +74,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
|
"vectorEmbeddingPolicy": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isAutoPilotSelected={false}
|
isAutoPilotSelected={false}
|
||||||
@@ -132,10 +136,13 @@ 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",
|
||||||
@@ -148,6 +155,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
|
"vectorEmbeddingPolicy": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
displayedTtlSeconds="5"
|
displayedTtlSeconds="5"
|
||||||
@@ -249,10 +257,13 @@ 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",
|
||||||
@@ -265,6 +276,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": {},
|
"uniqueKeyPolicy": {},
|
||||||
"usageSizeInKB": [Function],
|
"usageSizeInKB": [Function],
|
||||||
|
"vectorEmbeddingPolicy": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
explorer={
|
explorer={
|
||||||
@@ -330,6 +342,101 @@ 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,4 +1,5 @@
|
|||||||
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";
|
||||||
@@ -34,10 +35,23 @@ 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>(
|
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
||||||
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);
|
||||||
@@ -47,7 +61,6 @@ 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;
|
||||||
@@ -157,9 +170,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
|
|
||||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||||
if (mode === "Autoscale") {
|
if (mode === "Autoscale") {
|
||||||
const defaultThroughput = isFreeTier
|
|
||||||
? AutoPilotUtils.autoPilotThroughput1K
|
|
||||||
: AutoPilotUtils.autoPilotThroughput4K;
|
|
||||||
setThroughput(defaultThroughput);
|
setThroughput(defaultThroughput);
|
||||||
setIsAutoScaleSelected(true);
|
setIsAutoScaleSelected(true);
|
||||||
setThroughputValue(defaultThroughput);
|
setThroughputValue(defaultThroughput);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { useCallback } from "react";
|
|||||||
|
|
||||||
export interface TreeNodeMenuItem {
|
export interface TreeNodeMenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
onClick: () => void;
|
onClick: (value?: React.RefObject<HTMLElement>) => void;
|
||||||
iconSrc?: string;
|
iconSrc?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
@@ -74,6 +74,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
openItems,
|
openItems,
|
||||||
}: TreeNodeComponentProps): JSX.Element => {
|
}: TreeNodeComponentProps): JSX.Element => {
|
||||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||||
|
const contextMenuRef = React.useRef<HTMLButtonElement>(null);
|
||||||
const treeStyles = useTreeStyles();
|
const treeStyles = useTreeStyles();
|
||||||
|
|
||||||
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
|
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
|
||||||
@@ -107,7 +108,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
|
|
||||||
const onOpenChange = useCallback(
|
const onOpenChange = useCallback(
|
||||||
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
|
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
|
||||||
if (data.type === "Click" && node.onClick) {
|
if (data.type === "Click" && !isBranch && node.onClick) {
|
||||||
node.onClick();
|
node.onClick();
|
||||||
}
|
}
|
||||||
if (!node.isExpanded && data.open && node.onExpanded) {
|
if (!node.isExpanded && data.open && node.onExpanded) {
|
||||||
@@ -119,7 +120,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
node.onCollapsed?.();
|
node.onCollapsed?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[node, setIsLoading],
|
[isBranch, node, setIsLoading],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMenuOpenChange = useCallback(
|
const onMenuOpenChange = useCallback(
|
||||||
@@ -141,7 +142,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
|
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
|
||||||
disabled={menuItem.isDisabled}
|
disabled={menuItem.isDisabled}
|
||||||
key={menuItem.label}
|
key={menuItem.label}
|
||||||
onClick={menuItem.onClick}
|
onClick={() => menuItem.onClick(contextMenuRef)}
|
||||||
>
|
>
|
||||||
{menuItem.label}
|
{menuItem.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -190,6 +191,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
|
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
|
||||||
data-test="TreeNode/ContextMenuTrigger"
|
data-test="TreeNode/ContextMenuTrigger"
|
||||||
appearance="subtle"
|
appearance="subtle"
|
||||||
|
ref={contextMenuRef}
|
||||||
icon={<MoreHorizontal20Regular />}
|
icon={<MoreHorizontal20Regular />}
|
||||||
/>
|
/>
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
|
|||||||
@@ -1478,14 +1478,14 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
data-test="TreeNode/ContextMenuItem:enabledItem"
|
data-test="TreeNode/ContextMenuItem:enabledItem"
|
||||||
onClick={[MockFunction enabledItemClick]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
enabledItem
|
enabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
data-test="TreeNode/ContextMenuItem:disabledItem"
|
data-test="TreeNode/ContextMenuItem:disabledItem"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
onClick={[MockFunction disabledItemClick]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
disabledItem
|
disabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -1518,7 +1518,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
data-test="TreeNode/ContextMenuItem:enabledItem"
|
data-test="TreeNode/ContextMenuItem:enabledItem"
|
||||||
key="enabledItem"
|
key="enabledItem"
|
||||||
onClick={[MockFunction enabledItemClick]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
enabledItem
|
enabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -1526,7 +1526,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
data-test="TreeNode/ContextMenuItem:disabledItem"
|
data-test="TreeNode/ContextMenuItem:disabledItem"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
key="disabledItem"
|
key="disabledItem"
|
||||||
onClick={[MockFunction disabledItemClick]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
disabledItem
|
disabledItem
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
|
|||||||
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
|
import { VectorEmbeddingPoliciesComponent } from "./VectorEmbeddingPoliciesComponent";
|
||||||
|
|
||||||
const mockVectorEmbedding: VectorEmbedding[] = [
|
const mockVectorEmbedding: VectorEmbedding[] = [
|
||||||
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
||||||
@@ -17,9 +17,9 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
component = render(
|
component = render(
|
||||||
<AddVectorEmbeddingPolicyForm
|
<VectorEmbeddingPoliciesComponent
|
||||||
vectorEmbedding={mockVectorEmbedding}
|
vectorEmbeddings={mockVectorEmbedding}
|
||||||
vectorIndex={mockVectorIndex}
|
vectorIndexes={mockVectorIndex}
|
||||||
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -36,7 +36,7 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("calls onDelete when delete button is clicked", async () => {
|
test("calls onDelete when delete button is clicked", async () => {
|
||||||
const deleteButton = component.container.querySelector("#delete-vector-policy-1");
|
const deleteButton = component.container.querySelector("#delete-Vector-embedding-1");
|
||||||
fireEvent.click(deleteButton);
|
fireEvent.click(deleteButton);
|
||||||
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||||
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
||||||
@@ -49,21 +49,19 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
|||||||
|
|
||||||
test("validates input correctly", async () => {
|
test("validates input correctly", async () => {
|
||||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
|
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
|
||||||
await waitFor(() => expect(screen.getByText("Vector embedding path should not be empty")).toBeInTheDocument(), {
|
await waitFor(() => expect(screen.getByText("Path should not be empty")).toBeInTheDocument(), {
|
||||||
timeout: 1500,
|
timeout: 1500,
|
||||||
});
|
});
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() =>
|
() =>
|
||||||
expect(
|
expect(screen.getByText("Dimension must be greater than 0 and less than or equal 4096")).toBeInTheDocument(),
|
||||||
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
|
|
||||||
).toBeInTheDocument(),
|
|
||||||
{
|
{
|
||||||
timeout: 1500,
|
timeout: 1500,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
||||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
|
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
|
||||||
await waitFor(() => expect(screen.queryByText("Vector embedding path should not be empty")).toBeNull(), {
|
await waitFor(() => expect(screen.queryByText("Path should not be empty")).toBeNull(), {
|
||||||
timeout: 1500,
|
timeout: 1500,
|
||||||
});
|
});
|
||||||
await waitFor(
|
await waitFor(
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
import {
|
||||||
|
DefaultButton,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownOption,
|
||||||
|
IStyleFunctionOrObject,
|
||||||
|
ITextFieldStyleProps,
|
||||||
|
ITextFieldStyles,
|
||||||
|
Label,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
|
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
|
import {
|
||||||
|
getDataTypeOptions,
|
||||||
|
getDistanceFunctionOptions,
|
||||||
|
getIndexTypeOptions,
|
||||||
|
} from "Explorer/Controls/VectorSearch/VectorSearchUtils";
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
|
|
||||||
|
export interface IVectorEmbeddingPoliciesComponentProps {
|
||||||
|
vectorEmbeddings: VectorEmbedding[];
|
||||||
|
onVectorEmbeddingChange: (
|
||||||
|
vectorEmbeddings: VectorEmbedding[],
|
||||||
|
vectorIndexingPolicies: VectorIndex[],
|
||||||
|
validationPassed: boolean,
|
||||||
|
) => void;
|
||||||
|
vectorIndexes?: VectorIndex[];
|
||||||
|
discardChanges?: boolean;
|
||||||
|
onChangesDiscarded?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VectorEmbeddingPolicyData {
|
||||||
|
path: string;
|
||||||
|
dataType: VectorEmbedding["dataType"];
|
||||||
|
distanceFunction: VectorEmbedding["distanceFunction"];
|
||||||
|
dimensions: number;
|
||||||
|
indexType: VectorIndex["type"] | "none";
|
||||||
|
pathError: string;
|
||||||
|
dimensionsError: string;
|
||||||
|
diskANNShardKey?: string;
|
||||||
|
diskANNShardKeyError?: string;
|
||||||
|
indexingSearchListSize?: number;
|
||||||
|
indexingSearchListSizeError?: string;
|
||||||
|
quantizationByteSize?: number;
|
||||||
|
quantizationByteSizeError?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
|
||||||
|
|
||||||
|
const labelStyles = {
|
||||||
|
root: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||||
|
fieldGroup: {
|
||||||
|
height: 27,
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
fontSize: 12,
|
||||||
|
padding: "0 8px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdownStyles = {
|
||||||
|
title: {
|
||||||
|
height: 27,
|
||||||
|
lineHeight: "24px",
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
height: 27,
|
||||||
|
lineHeight: "24px",
|
||||||
|
},
|
||||||
|
dropdownItem: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddingPoliciesComponentProps> = ({
|
||||||
|
vectorEmbeddings,
|
||||||
|
vectorIndexes,
|
||||||
|
onVectorEmbeddingChange,
|
||||||
|
discardChanges,
|
||||||
|
onChangesDiscarded,
|
||||||
|
disabled,
|
||||||
|
}): JSX.Element => {
|
||||||
|
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
|
||||||
|
let error = "";
|
||||||
|
if (!path) {
|
||||||
|
error = "Path should not be empty";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
index >= 0 &&
|
||||||
|
vectorEmbeddingPolicyData?.find(
|
||||||
|
(vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) =>
|
||||||
|
dataIndex !== index && vectorEmbedding.path === path,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
error = "Path is already defined";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
|
||||||
|
let error = "";
|
||||||
|
if (dimension <= 0 || dimension > 4096) {
|
||||||
|
error = "Dimension must be greater than 0 and less than or equal 4096";
|
||||||
|
}
|
||||||
|
if (indexType === "flat" && dimension > 505) {
|
||||||
|
error = "Maximum allowed dimension for flat index is 505";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onQuantizationByteSizeError = (size: number): string => {
|
||||||
|
let error = "";
|
||||||
|
if (size < 1 || size > 512) {
|
||||||
|
error = "Quantization byte size must be greater than 0 and less than or equal to 512";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onIndexingSearchListSizeError = (size: number): string => {
|
||||||
|
let error = "";
|
||||||
|
if (size < 25 || size > 500) {
|
||||||
|
error = "Indexing search list size must be greater than or equal to 25 and less than or equal to 500";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
//TODO: no restrictions yet due to this field being removed for now.
|
||||||
|
// Uncomment and replace with validation code when field is reinstated
|
||||||
|
// const onDiskANNShardKeyError = (shardKey: string): string => {
|
||||||
|
// return "";
|
||||||
|
// };
|
||||||
|
|
||||||
|
const initializeData = (vectorEmbeddings: VectorEmbedding[], vectorIndexes: VectorIndex[]) => {
|
||||||
|
const mergedData: VectorEmbeddingPolicyData[] = [];
|
||||||
|
vectorEmbeddings.forEach((embedding) => {
|
||||||
|
const matchingIndex = displayIndexes ? vectorIndexes.find((index) => index.path === embedding.path) : undefined;
|
||||||
|
mergedData.push({
|
||||||
|
...embedding,
|
||||||
|
indexType: matchingIndex?.type || "none",
|
||||||
|
indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined,
|
||||||
|
quantizationByteSize: matchingIndex?.quantizationByteSize || undefined,
|
||||||
|
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||||
|
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return mergedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [displayIndexes] = useState<boolean>(!!vectorIndexes);
|
||||||
|
const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState<VectorEmbeddingPolicyData[]>(
|
||||||
|
initializeData(vectorEmbeddings, vectorIndexes),
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
propagateData();
|
||||||
|
}, [vectorEmbeddingPolicyData]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (discardChanges) {
|
||||||
|
setVectorEmbeddingPolicyData(initializeData(vectorEmbeddings, vectorIndexes));
|
||||||
|
onChangesDiscarded();
|
||||||
|
}
|
||||||
|
}, [discardChanges]);
|
||||||
|
|
||||||
|
const propagateData = () => {
|
||||||
|
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
|
||||||
|
path: policy.path,
|
||||||
|
dataType: policy.dataType,
|
||||||
|
dimensions: policy.dimensions,
|
||||||
|
distanceFunction: policy.distanceFunction,
|
||||||
|
}));
|
||||||
|
const vectorIndexes: VectorIndex[] = vectorEmbeddingPolicyData
|
||||||
|
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
|
||||||
|
.map(
|
||||||
|
(policy) =>
|
||||||
|
({
|
||||||
|
path: policy.path,
|
||||||
|
type: policy.indexType,
|
||||||
|
indexingSearchListSize: policy.indexingSearchListSize,
|
||||||
|
quantizationByteSize: policy.quantizationByteSize,
|
||||||
|
}) as VectorIndex,
|
||||||
|
);
|
||||||
|
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||||
|
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
|
||||||
|
);
|
||||||
|
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value.trim();
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
if (!vectorEmbeddings[index]?.path && !value.startsWith("/")) {
|
||||||
|
vectorEmbeddings[index].path = "/" + value;
|
||||||
|
} else {
|
||||||
|
vectorEmbeddings[index].path = value;
|
||||||
|
}
|
||||||
|
const error = onVectorEmbeddingPathError(value, index);
|
||||||
|
vectorEmbeddings[index].pathError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingDimensionsChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = parseInt(event.target.value.trim()) || 0;
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
const vectorEmbedding = vectorEmbeddings[index];
|
||||||
|
vectorEmbeddings[index].dimensions = value;
|
||||||
|
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
|
||||||
|
vectorEmbeddings[index].dimensionsError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingIndexTypeChange = (index: number, option: IDropdownOption): void => {
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
const vectorEmbedding = vectorEmbeddings[index];
|
||||||
|
vectorEmbeddings[index].indexType = option.key as never;
|
||||||
|
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
|
||||||
|
vectorEmbeddings[index].dimensionsError = error;
|
||||||
|
if (vectorEmbedding.indexType === "diskANN") {
|
||||||
|
vectorEmbedding.indexingSearchListSize = 100;
|
||||||
|
} else {
|
||||||
|
vectorEmbedding.indexingSearchListSize = undefined;
|
||||||
|
}
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onQuantizationByteSizeChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = parseInt(event.target.value.trim()) || 0;
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
vectorEmbeddings[index].quantizationByteSize = value;
|
||||||
|
vectorEmbeddings[index].quantizationByteSizeError = onQuantizationByteSizeError(value);
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onIndexingSearchListSizeChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = parseInt(event.target.value.trim()) || 0;
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
vectorEmbeddings[index].indexingSearchListSize = value;
|
||||||
|
vectorEmbeddings[index].indexingSearchListSizeError = onIndexingSearchListSizeError(value);
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: uncomment after Ignite
|
||||||
|
// DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite
|
||||||
|
// const onDiskANNShardKeyChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
// const value = event.target.value.trim();
|
||||||
|
// const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
// if (!vectorEmbeddings[index]?.diskANNShardKey && !value.startsWith("/")) {
|
||||||
|
// vectorEmbeddings[index].diskANNShardKey = "/" + value;
|
||||||
|
// } else {
|
||||||
|
// vectorEmbeddings[index].diskANNShardKey = value;
|
||||||
|
// }
|
||||||
|
// const error = onDiskANNShardKeyError(value);
|
||||||
|
// vectorEmbeddings[index].diskANNShardKeyError = error;
|
||||||
|
// setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
// }
|
||||||
|
|
||||||
|
const onVectorEmbeddingPolicyChange = (
|
||||||
|
index: number,
|
||||||
|
option: IDropdownOption,
|
||||||
|
property: VectorEmbeddingPolicyProperty,
|
||||||
|
): void => {
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
vectorEmbeddings[index][property] = option.key as never;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAdd = () => {
|
||||||
|
setVectorEmbeddingPolicyData([
|
||||||
|
...vectorEmbeddingPolicyData,
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
dataType: "float32",
|
||||||
|
distanceFunction: "euclidean",
|
||||||
|
dimensions: 0,
|
||||||
|
indexType: "none",
|
||||||
|
pathError: onVectorEmbeddingPathError(""),
|
||||||
|
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (index: number) => {
|
||||||
|
const vectorEmbeddings = vectorEmbeddingPolicyData.filter((_uniqueKey, j) => index !== j);
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
|
{vectorEmbeddingPolicyData &&
|
||||||
|
vectorEmbeddingPolicyData.length > 0 &&
|
||||||
|
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
|
||||||
|
<CollapsibleSectionComponent
|
||||||
|
disabled={disabled}
|
||||||
|
key={index}
|
||||||
|
isExpandedByDefault={true}
|
||||||
|
title={`Vector embedding ${index + 1}`}
|
||||||
|
showDelete={true}
|
||||||
|
onDelete={() => onDelete(index)}
|
||||||
|
>
|
||||||
|
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||||
|
<Stack
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
margin: "0 0 6px 20px !important",
|
||||||
|
paddingLeft: 20,
|
||||||
|
width: "80%",
|
||||||
|
borderLeft: "1px solid",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Label disabled={disabled} styles={labelStyles}>
|
||||||
|
Path
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
disabled={disabled}
|
||||||
|
id={`vector-policy-path-${index + 1}`}
|
||||||
|
required={true}
|
||||||
|
placeholder="/vector1"
|
||||||
|
styles={textFieldStyles}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onVectorEmbeddingPathChange(index, event)}
|
||||||
|
value={vectorEmbeddingPolicy.path || ""}
|
||||||
|
errorMessage={vectorEmbeddingPolicy.pathError}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label disabled={disabled} styles={labelStyles}>
|
||||||
|
Data type
|
||||||
|
</Label>
|
||||||
|
<Dropdown
|
||||||
|
disabled={disabled}
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getDataTypeOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.dataType}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingPolicyChange(index, option, "dataType")
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label disabled={disabled} styles={labelStyles}>
|
||||||
|
Distance function
|
||||||
|
</Label>
|
||||||
|
<Dropdown
|
||||||
|
disabled={disabled}
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getDistanceFunctionOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.distanceFunction}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingPolicyChange(index, option, "distanceFunction")
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label disabled={disabled} styles={labelStyles}>
|
||||||
|
Dimensions
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
disabled={disabled}
|
||||||
|
id={`vector-policy-dimension-${index + 1}`}
|
||||||
|
required={true}
|
||||||
|
styles={textFieldStyles}
|
||||||
|
value={String(vectorEmbeddingPolicy.dimensions || 0)}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onVectorEmbeddingDimensionsChange(index, event)
|
||||||
|
}
|
||||||
|
errorMessage={vectorEmbeddingPolicy.dimensionsError}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
{displayIndexes && (
|
||||||
|
<Stack>
|
||||||
|
<Label disabled={disabled} styles={labelStyles}>
|
||||||
|
Index type
|
||||||
|
</Label>
|
||||||
|
<Dropdown
|
||||||
|
disabled={disabled}
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getIndexTypeOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.indexType}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingIndexTypeChange(index, option)
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
<Stack style={{ marginLeft: "10px" }}>
|
||||||
|
<Label
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
(vectorEmbeddingPolicy.indexType !== "quantizedFlat" &&
|
||||||
|
vectorEmbeddingPolicy.indexType !== "diskANN")
|
||||||
|
}
|
||||||
|
styles={labelStyles}
|
||||||
|
>
|
||||||
|
Quantization byte size
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
(vectorEmbeddingPolicy.indexType !== "quantizedFlat" &&
|
||||||
|
vectorEmbeddingPolicy.indexType !== "diskANN")
|
||||||
|
}
|
||||||
|
id={`vector-policy-quantizationByteSize-${index + 1}`}
|
||||||
|
styles={textFieldStyles}
|
||||||
|
value={String(vectorEmbeddingPolicy.quantizationByteSize || "")}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onQuantizationByteSizeChange(index, event)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack style={{ marginLeft: "10px" }}>
|
||||||
|
<Label disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"} styles={labelStyles}>
|
||||||
|
Indexing search list size
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
|
||||||
|
id={`vector-policy-indexingSearchListSize-${index + 1}`}
|
||||||
|
styles={textFieldStyles}
|
||||||
|
value={String(vectorEmbeddingPolicy.indexingSearchListSize || "")}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onIndexingSearchListSizeChange(index, event)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
{/*TODO: uncomment after Ignite */}
|
||||||
|
{/* DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite
|
||||||
|
<Stack
|
||||||
|
style={{ marginLeft: "10px" }}
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
|
||||||
|
styles={labelStyles}
|
||||||
|
>DiskANN shard key</Label>
|
||||||
|
<TextField
|
||||||
|
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
|
||||||
|
id={`vector-policy-diskANNShardKey-${index + 1}`}
|
||||||
|
styles={textFieldStyles}
|
||||||
|
value={String(vectorEmbeddingPolicy.diskANNShardKey || "")}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onDiskANNShardKeyChange(index, event)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
*/}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
))}
|
||||||
|
<DefaultButton
|
||||||
|
disabled={disabled}
|
||||||
|
id={`add-vector-policy`}
|
||||||
|
styles={{ root: { maxWidth: 170, fontSize: 12 } }}
|
||||||
|
onClick={onAdd}
|
||||||
|
>
|
||||||
|
Add vector embedding
|
||||||
|
</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,13 +5,12 @@ import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
|||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
|
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -36,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 { isAccountNewerThanThresholdInMs, updateUserContext, userContext } from "../UserContext";
|
import { 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";
|
||||||
@@ -44,10 +43,11 @@ 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 { useTabs } from "../hooks/useTabs";
|
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
|
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||||
@@ -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";
|
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/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,6 +187,10 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,25 +263,8 @@ export default class Explorer {
|
|||||||
|
|
||||||
public async openLoginForEntraIDPopUp(): Promise<void> {
|
public async openLoginForEntraIDPopUp(): Promise<void> {
|
||||||
if (userContext.databaseAccount.properties?.documentEndpoint) {
|
if (userContext.databaseAccount.properties?.documentEndpoint) {
|
||||||
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
|
||||||
/\/$/,
|
|
||||||
"/.default",
|
|
||||||
);
|
|
||||||
const msalInstance = await getMsalInstance();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await msalInstance.loginPopup({
|
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, false);
|
||||||
redirectUri: configContext.msalRedirectURI,
|
|
||||||
scopes: [],
|
|
||||||
});
|
|
||||||
localStorage.setItem("cachedTenantId", response.tenantId);
|
|
||||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
|
||||||
msalInstance.setActiveAccount(cachedAccount);
|
|
||||||
const aadToken = await acquireTokenWithMsal(msalInstance, {
|
|
||||||
forceRefresh: true,
|
|
||||||
scopes: [hrefEndpoint],
|
|
||||||
authority: `${configContext.AAD_ENDPOINT}${localStorage.getItem("cachedTenantId")}`,
|
|
||||||
});
|
|
||||||
updateUserContext({ aadToken: aadToken });
|
updateUserContext({ aadToken: aadToken });
|
||||||
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
useDataPlaneRbac.setState({ aadTokenUpdated: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -295,37 +282,6 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openNPSSurveyDialog(): void {
|
|
||||||
if (!Platform.Portal || !["Postgres", "SQL", "Mongo"].includes(userContext.apiType)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ONE_DAY_IN_MS = 86400000;
|
|
||||||
const SEVEN_DAYS_IN_MS = 604800000;
|
|
||||||
|
|
||||||
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
|
|
||||||
if (userContext.isTryCosmosDBSubscription) {
|
|
||||||
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
|
|
||||||
Logger.logInfo(
|
|
||||||
`Sending message to Portal to check if NPS Survey can be displayed in Try Cosmos DB ${userContext.apiType}`,
|
|
||||||
"Explorer/openNPSSurveyDialog",
|
|
||||||
);
|
|
||||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Show survey when an existing account is older than 7 days
|
|
||||||
if (
|
|
||||||
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", SEVEN_DAYS_IN_MS)
|
|
||||||
) {
|
|
||||||
Logger.logInfo(
|
|
||||||
`Sending message to Portal to check if NPS Survey can be displayed for existing ${userContext.apiType} account older than 7 days`,
|
|
||||||
"Explorer/openNPSSurveyDialog",
|
|
||||||
);
|
|
||||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async openCESCVAFeedbackBlade(): Promise<void> {
|
public async openCESCVAFeedbackBlade(): Promise<void> {
|
||||||
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
@@ -395,8 +351,8 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||||
if (configContext.platform === Platform.Fabric) {
|
if (isFabricMirroredKey()) {
|
||||||
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
|
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1151,7 +1107,7 @@ export default class Explorer {
|
|||||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
||||||
}
|
}
|
||||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||||
|
|
||||||
@@ -1175,7 +1131,7 @@ export default class Explorer {
|
|||||||
await this.initNotebooks(userContext.databaseAccount);
|
await this.initNotebooks(userContext.databaseAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.refreshSampleData();
|
this.refreshSampleData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async configureCopilot(): Promise<void> {
|
public async configureCopilot(): Promise<void> {
|
||||||
@@ -1200,26 +1156,27 @@ export default class Explorer {
|
|||||||
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refreshSampleData(): Promise<void> {
|
public refreshSampleData(): void {
|
||||||
try {
|
if (!userContext.sampleDataConnectionInfo) {
|
||||||
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,10 +14,6 @@
|
|||||||
.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;
|
||||||
@@ -63,6 +59,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customTrashIcon {
|
||||||
|
padding-top: 33px;
|
||||||
|
}
|
||||||
|
|
||||||
.rightPaneTrashIconImg {
|
.rightPaneTrashIconImg {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,10 +142,11 @@ 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}
|
||||||
aria-required="true"
|
required
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
aria-label={`Enter value for propery ${index + 1}`}
|
aria-label={`Enter value for propery ${index + 1}`}
|
||||||
@@ -153,11 +154,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"
|
||||||
@@ -169,6 +170,8 @@ 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 }}
|
||||||
@@ -181,7 +184,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
</div>
|
</div>
|
||||||
<div className="actionCol">
|
<div className="actionCol">
|
||||||
<div
|
<div
|
||||||
className="rightPaneTrashIcon rightPaneBtns"
|
className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
aria-label={`Delete ${data.key}`}
|
aria-label={`Delete ${data.key}`}
|
||||||
|
|||||||
@@ -4,51 +4,83 @@
|
|||||||
* and update any knockout observables passed from the parent.
|
* and update any knockout observables passed from the parent.
|
||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import {
|
|
||||||
createPlatformButtons,
|
|
||||||
createStaticCommandBarButtons,
|
|
||||||
} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory";
|
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
|
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import create, { UseStore } from "zustand";
|
||||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
import { Platform, configContext } from "../../../ConfigContext";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
|
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||||
import * as CommandBarUtil from "./CommandBarUtil";
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CommandBarStore {
|
||||||
|
contextButtons: CommandButtonComponentProps[];
|
||||||
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
||||||
|
isHidden: boolean;
|
||||||
|
setIsHidden: (isHidden: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
||||||
|
contextButtons: [],
|
||||||
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||||
|
isHidden: false,
|
||||||
|
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
||||||
|
}));
|
||||||
|
|
||||||
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||||
const selectedNodeState = useSelectedNode();
|
const selectedNodeState = useSelectedNode();
|
||||||
const contextButtons = useCommandBar((state) => state.contextButtons);
|
const buttons = useCommandBar((state) => state.contextButtons);
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
const isHidden = useCommandBar((state) => state.isHidden);
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||||
const staticButtons = createStaticCommandBarButtons(selectedNodeState);
|
|
||||||
const platformButtons = createPlatformButtons();
|
|
||||||
|
|
||||||
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor, container);
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
if (contextButtons?.length > 0) {
|
const buttons =
|
||||||
|
userContext.apiType === "Postgres"
|
||||||
|
? CommandBarComponentButtonFactory.createPostgreButtons(container)
|
||||||
|
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
|
||||||
|
return (
|
||||||
|
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||||
|
<FluentCommandBar
|
||||||
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
|
items={CommandBarUtil.convertButton(buttons, backgroundColor)}
|
||||||
|
styles={{
|
||||||
|
root: { backgroundColor: backgroundColor },
|
||||||
|
}}
|
||||||
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
|
||||||
|
const contextButtons = (buttons || []).concat(
|
||||||
|
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
|
||||||
|
);
|
||||||
|
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
|
||||||
|
|
||||||
|
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
|
||||||
|
if (buttons && buttons.length > 0) {
|
||||||
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(
|
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
|
||||||
contextButtons || [],
|
|
||||||
backgroundColor,
|
|
||||||
container,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (uiFabricTabsButtons.length > 0) {
|
if (uiFabricTabsButtons.length > 0) {
|
||||||
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricPlatformButtons = CommandBarUtil.convertButton(platformButtons || [], backgroundColor, container);
|
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||||
uiFabricPlatformButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
|
|
||||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
|
|
||||||
@@ -56,27 +88,26 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
|
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
|
||||||
connectionInfo?.status !== ConnectionStatusType.Connect
|
connectionInfo?.status !== ConnectionStatusType.Connect
|
||||||
) {
|
) {
|
||||||
uiFabricPlatformButtons.unshift(
|
uiFabricControlButtons.unshift(
|
||||||
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
|
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootStyle =
|
const rootStyle = isFabric()
|
||||||
configContext.platform === Platform.Fabric
|
? {
|
||||||
? {
|
root: {
|
||||||
root: {
|
backgroundColor: "transparent",
|
||||||
backgroundColor: "transparent",
|
padding: "2px 8px 0px 8px",
|
||||||
padding: "2px 8px 0px 8px",
|
},
|
||||||
},
|
}
|
||||||
}
|
: {
|
||||||
: {
|
root: {
|
||||||
root: {
|
backgroundColor: backgroundColor,
|
||||||
backgroundColor: backgroundColor,
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
|
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||||
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons, container);
|
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||||
setKeyboardHandlers(keyboardHandlers);
|
setKeyboardHandlers(keyboardHandlers);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,7 +115,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||||
farItems={uiFabricPlatformButtons}
|
farItems={uiFabricControlButtons}
|
||||||
styles={rootStyle}
|
styles={rootStyle}
|
||||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { AuthType } from "../../../AuthType";
|
|||||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||||
import { updateUserContext } from "../../../UserContext";
|
import { updateUserContext } from "../../../UserContext";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||||
|
|
||||||
describe("CommandBarComponentButtonFactory tests", () => {
|
describe("CommandBarComponentButtonFactory tests", () => {
|
||||||
|
let mockExplorer: Explorer;
|
||||||
|
|
||||||
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
|
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
|
||||||
|
|
||||||
describe("Enable Azure Synapse Link Button", () => {
|
describe("Enable Azure Synapse Link Button", () => {
|
||||||
@@ -16,6 +19,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -26,28 +30,32 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Button should be visible", () => {
|
it("Button should be visible", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Button should not be visible for Tables API", () => {
|
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
|
||||||
updateUserContext({
|
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
|
||||||
databaseAccount: {
|
// unsupported in jest and needs to be tested with react-hooks-testing-library.
|
||||||
properties: {
|
//
|
||||||
capabilities: [{ name: "EnableTable" }],
|
// it("Button should not be visible for Tables API", () => {
|
||||||
},
|
// updateUserContext({
|
||||||
} as DatabaseAccount,
|
// databaseAccount: {
|
||||||
});
|
// properties: {
|
||||||
|
// capabilities: [{ name: "EnableTable" }],
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
// },
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
// } as DatabaseAccount,
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
// });
|
||||||
);
|
//
|
||||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
});
|
// 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({
|
||||||
@@ -58,7 +66,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -71,6 +79,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -103,7 +112,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
},
|
},
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -113,13 +122,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
portalEnv: "mooncake",
|
portalEnv: "mooncake",
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -129,8 +138,12 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const openPostgresShellButtonLabel = "Open PSQL shell";
|
const openPostgresShellButtonLabel = "Open PSQL shell";
|
||||||
const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell";
|
const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell";
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
|
});
|
||||||
|
|
||||||
it("creates Postgres shell button", () => {
|
it("creates Postgres shell button", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createPostgreButtons();
|
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(mockExplorer);
|
||||||
const openPostgresShellButton = buttons.find(
|
const openPostgresShellButton = buttons.find(
|
||||||
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
|
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
|
||||||
);
|
);
|
||||||
@@ -138,7 +151,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("creates vCore Mongo shell button", () => {
|
it("creates vCore Mongo shell button", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons();
|
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(mockExplorer);
|
||||||
const openVCoreMongoShellButton = buttons.find(
|
const openVCoreMongoShellButton = buttons.find(
|
||||||
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
|
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
|
||||||
);
|
);
|
||||||
@@ -153,6 +166,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
authType: AuthType.ResourceToken,
|
authType: AuthType.ResourceToken,
|
||||||
});
|
});
|
||||||
@@ -164,7 +179,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
kind: "DocumentDB",
|
kind: "DocumentDB",
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
expect(buttons.length).toBe(2);
|
expect(buttons.length).toBe(2);
|
||||||
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
||||||
expect(buttons[0].disabled).toBe(false);
|
expect(buttons[0].disabled).toBe(false);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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";
|
||||||
@@ -21,6 +22,7 @@ import { userContext } from "../../../UserContext";
|
|||||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
@@ -31,20 +33,19 @@ import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
|
|||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
|
export function createStaticCommandBarButtons(
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
container: Explorer,
|
||||||
return userContext.apiType === "Postgres" ? createPostgreButtons() : createVCoreMongoButtons();
|
selectedNodeState: SelectedNodeState,
|
||||||
}
|
): CommandButtonComponentProps[] {
|
||||||
|
|
||||||
if (userContext.authType === AuthType.ResourceToken) {
|
if (userContext.authType === AuthType.ResourceToken) {
|
||||||
return createStaticCommandBarButtonsForResourceToken(selectedNodeState);
|
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
// Avoid starting with a divider
|
// Avoid starting with a divider
|
||||||
const addDivider = () => {
|
const addDivider = () => {
|
||||||
if (buttons.length > 0 && !buttons[buttons.length - 1].isDivider) {
|
if (buttons.length > 0) {
|
||||||
buttons.push(createDivider());
|
buttons.push(createDivider());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,22 +55,22 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables" &&
|
||||||
userContext.apiType !== "Cassandra"
|
userContext.apiType !== "Cassandra"
|
||||||
) {
|
) {
|
||||||
const addSynapseLink = createOpenSynapseLinkDialogButton();
|
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||||
if (addSynapseLink) {
|
if (addSynapseLink) {
|
||||||
addDivider();
|
addDivider();
|
||||||
buttons.push(addSynapseLink);
|
buttons.push(addSynapseLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.apiType === "SQL") {
|
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||||
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);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buttonProps = createLoginForEntraIDButton();
|
const buttonProps = createLoginForEntraIDButton(container);
|
||||||
setLoginButtonProps(buttonProps);
|
setLoginButtonProps(buttonProps);
|
||||||
}, [dataPlaneRbacEnabled, aadTokenUpdated]);
|
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
|
||||||
|
|
||||||
if (loginButtonProps) {
|
if (loginButtonProps) {
|
||||||
addDivider();
|
addDivider();
|
||||||
@@ -87,8 +88,8 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
||||||
const openQueryBtn = createOpenQueryButton();
|
const openQueryBtn = createOpenQueryButton(container);
|
||||||
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||||
buttons.push(openQueryBtn);
|
buttons.push(openQueryBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +104,6 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
tooltipText: userContext.features.commandBarV2 ? "New..." : label,
|
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
disabled:
|
disabled:
|
||||||
@@ -116,12 +116,21 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createContextCommandBarButtons(
|
||||||
|
container: Explorer,
|
||||||
|
selectedNodeState: SelectedNodeState,
|
||||||
|
): CommandButtonComponentProps[] {
|
||||||
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||||
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
||||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled) {
|
if (useNotebook.getState().isShellEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
@@ -133,7 +142,6 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
};
|
};
|
||||||
addDivider();
|
|
||||||
buttons.push(newMongoShellBtn);
|
buttons.push(newMongoShellBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,27 +154,25 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
const newCassandraShellButton: CommandButtonComponentProps = {
|
const newCassandraShellButton: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => {
|
onCommandClick: () => {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
};
|
};
|
||||||
addDivider();
|
|
||||||
buttons.push(newCassandraShellButton);
|
buttons.push(newCassandraShellButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPlatformButtons(): CommandButtonComponentProps[] {
|
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] = [
|
const buttons: CommandButtonComponentProps[] = [
|
||||||
{
|
{
|
||||||
iconSrc: SettingsIcon,
|
iconSrc: SettingsIcon,
|
||||||
iconAlt: "Settings",
|
iconAlt: "Settings",
|
||||||
onCommandClick: (_, container) =>
|
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
||||||
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: "Settings",
|
ariaLabel: "Settings",
|
||||||
tooltipText: "Settings",
|
tooltipText: "Settings",
|
||||||
@@ -202,7 +208,7 @@ export function createPlatformButtons(): CommandButtonComponentProps[] {
|
|||||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||||
iconSrc: FeedbackIcon,
|
iconSrc: FeedbackIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => container.openCESCVAFeedbackBlade(),
|
onCommandClick: () => container.openCESCVAFeedbackBlade(),
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
tooltipText: label,
|
tooltipText: label,
|
||||||
@@ -234,7 +240,7 @@ function areScriptsSupported(): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
|
||||||
if (configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -252,7 +258,7 @@ function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
|||||||
return {
|
return {
|
||||||
iconSrc: SynapseIcon,
|
iconSrc: SynapseIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => container.openEnableSynapseLinkDialog(),
|
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled:
|
disabled:
|
||||||
@@ -261,12 +267,12 @@ function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLoginForEntraIDButton(): CommandButtonComponentProps {
|
function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps {
|
||||||
if (configContext.platform !== Platform.Portal) {
|
if (configContext.platform !== Platform.Portal) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCommandClick: CommandButtonComponentProps["onCommandClick"] = async (_, container) => {
|
const handleCommandClick = async () => {
|
||||||
await container.openLoginForEntraIDPopUp();
|
await container.openLoginForEntraIDPopUp();
|
||||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
};
|
};
|
||||||
@@ -393,14 +399,13 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
|||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenQueryButton(): CommandButtonComponentProps {
|
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
|
||||||
const label = "Open Query";
|
const label = "Open Query";
|
||||||
return {
|
return {
|
||||||
iconSrc: BrowseQueriesIcon,
|
iconSrc: BrowseQueriesIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
tooltipText: userContext.features.commandBarV2 ? "Open Query..." : "Open Query",
|
|
||||||
keyboardAction: KeyboardAction.OPEN_QUERY,
|
keyboardAction: KeyboardAction.OPEN_QUERY,
|
||||||
onCommandClick: (_, container) =>
|
onCommandClick: () =>
|
||||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@@ -423,7 +428,10 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind): CommandButtonComponentProps {
|
function createOpenTerminalButtonByKind(
|
||||||
|
container: Explorer,
|
||||||
|
terminalKind: ViewModels.TerminalKind,
|
||||||
|
): CommandButtonComponentProps {
|
||||||
const terminalFriendlyName = (): string => {
|
const terminalFriendlyName = (): string => {
|
||||||
switch (terminalKind) {
|
switch (terminalKind) {
|
||||||
case ViewModels.TerminalKind.Cassandra:
|
case ViewModels.TerminalKind.Cassandra:
|
||||||
@@ -446,7 +454,7 @@ function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind):
|
|||||||
return {
|
return {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => {
|
onCommandClick: () => {
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
container.openNotebookTerminal(terminalKind);
|
container.openNotebookTerminal(terminalKind);
|
||||||
}
|
}
|
||||||
@@ -460,10 +468,11 @@ function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createStaticCommandBarButtonsForResourceToken(
|
function createStaticCommandBarButtonsForResourceToken(
|
||||||
|
container: Explorer,
|
||||||
selectedNodeState: SelectedNodeState,
|
selectedNodeState: SelectedNodeState,
|
||||||
): CommandButtonComponentProps[] {
|
): CommandButtonComponentProps[] {
|
||||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||||
const openQueryBtn = createOpenQueryButton();
|
const openQueryBtn = createOpenQueryButton(container);
|
||||||
|
|
||||||
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
||||||
const isResourceTokenCollectionNodeSelected: boolean =
|
const isResourceTokenCollectionNodeSelected: boolean =
|
||||||
@@ -476,20 +485,20 @@ function createStaticCommandBarButtonsForResourceToken(
|
|||||||
|
|
||||||
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
||||||
if (!openQueryBtn.disabled) {
|
if (!openQueryBtn.disabled) {
|
||||||
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [newSqlQueryBtn, openQueryBtn];
|
return [newSqlQueryBtn, openQueryBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPostgreButtons(): CommandButtonComponentProps[] {
|
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const openPostgreShellBtn = createOpenTerminalButtonByKind(ViewModels.TerminalKind.Postgres);
|
const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres);
|
||||||
|
|
||||||
return [openPostgreShellBtn];
|
return [openPostgreShellBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createVCoreMongoButtons(): CommandButtonComponentProps[] {
|
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(ViewModels.TerminalKind.VCoreMongo);
|
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
|
||||||
|
|
||||||
return [openVCoreMongoTerminalButton];
|
return [openVCoreMongoTerminalButton];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { ICommandBarItemProps } from "@fluentui/react";
|
import { ICommandBarItemProps } from "@fluentui/react";
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import * as CommandBarUtil from "./CommandBarUtil";
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
|
|
||||||
describe("CommandBarUtil tests", () => {
|
describe("CommandBarUtil tests", () => {
|
||||||
const mockExplorer = {} as Explorer;
|
|
||||||
const createButton = (): CommandButtonComponentProps => {
|
const createButton = (): CommandButtonComponentProps => {
|
||||||
return {
|
return {
|
||||||
iconSrc: "icon",
|
iconSrc: "icon",
|
||||||
@@ -24,7 +22,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
const btn = createButton();
|
const btn = createButton();
|
||||||
const backgroundColor = "backgroundColor";
|
const backgroundColor = "backgroundColor";
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer);
|
const converteds = CommandBarUtil.convertButton([btn], backgroundColor);
|
||||||
expect(converteds.length).toBe(1);
|
expect(converteds.length).toBe(1);
|
||||||
const converted = converteds[0];
|
const converted = converteds[0];
|
||||||
expect(converted.split).toBe(undefined);
|
expect(converted.split).toBe(undefined);
|
||||||
@@ -48,7 +46,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
btn.children.push(child);
|
btn.children.push(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor", mockExplorer);
|
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor");
|
||||||
expect(converteds.length).toBe(1);
|
expect(converteds.length).toBe(1);
|
||||||
const converted = converteds[0];
|
const converted = converteds[0];
|
||||||
expect(converted.split).toBe(true);
|
expect(converted.split).toBe(true);
|
||||||
@@ -64,7 +62,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
btns.push(createButton());
|
btns.push(createButton());
|
||||||
}
|
}
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor", mockExplorer);
|
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor");
|
||||||
const uniqueKeys = converteds
|
const uniqueKeys = converteds
|
||||||
.map((btn: ICommandBarItemProps) => btn.key)
|
.map((btn: ICommandBarItemProps) => btn.key)
|
||||||
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
|
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
|
||||||
@@ -76,10 +74,10 @@ describe("CommandBarUtil tests", () => {
|
|||||||
const backgroundColor = "backgroundColor";
|
const backgroundColor = "backgroundColor";
|
||||||
|
|
||||||
btn.commandButtonLabel = undefined;
|
btn.commandButtonLabel = undefined;
|
||||||
let converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
|
let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
||||||
expect(converted.text).toEqual(btn.tooltipText);
|
expect(converted.text).toEqual(btn.tooltipText);
|
||||||
|
|
||||||
converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
|
converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
||||||
delete btn.commandButtonLabel;
|
delete btn.commandButtonLabel;
|
||||||
expect(converted.text).toEqual(btn.tooltipText);
|
expect(converted.text).toEqual(btn.tooltipText);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,11 +25,7 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
|
|||||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||||
* @param btns
|
* @param btns
|
||||||
*/
|
*/
|
||||||
export const convertButton = (
|
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||||
btns: CommandButtonComponentProps[],
|
|
||||||
backgroundColor: string,
|
|
||||||
container: Explorer,
|
|
||||||
): ICommandBarItemProps[] => {
|
|
||||||
const buttonHeightPx =
|
const buttonHeightPx =
|
||||||
configContext.platform == Platform.Fabric
|
configContext.platform == Platform.Fabric
|
||||||
? StyleConstants.FabricCommandBarButtonHeight
|
? StyleConstants.FabricCommandBarButtonHeight
|
||||||
@@ -58,14 +54,15 @@ export const convertButton = (
|
|||||||
iconProps: {
|
iconProps: {
|
||||||
style: {
|
style: {
|
||||||
width: StyleConstants.CommandBarIconWidth, // 16
|
width: StyleConstants.CommandBarIconWidth, // 16
|
||||||
alignSelf: undefined,
|
alignSelf: btn.iconName ? "baseline" : undefined,
|
||||||
filter: getFilter(btn.disabled),
|
filter: getFilter(btn.disabled),
|
||||||
},
|
},
|
||||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||||
|
iconName: btn.iconName,
|
||||||
},
|
},
|
||||||
onClick: btn.onCommandClick
|
onClick: btn.onCommandClick
|
||||||
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||||
btn.onCommandClick(ev, container);
|
btn.onCommandClick(ev);
|
||||||
let copilotEnabled = false;
|
let copilotEnabled = false;
|
||||||
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
||||||
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
||||||
@@ -138,7 +135,7 @@ export const convertButton = (
|
|||||||
result.split = true;
|
result.split = true;
|
||||||
|
|
||||||
result.subMenuProps = {
|
result.subMenuProps = {
|
||||||
items: convertButton(btn.children, backgroundColor, container),
|
items: convertButton(btn.children, backgroundColor),
|
||||||
styles: {
|
styles: {
|
||||||
list: {
|
list: {
|
||||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||||
@@ -189,7 +186,7 @@ export const convertButton = (
|
|||||||
option?: IDropdownOption,
|
option?: IDropdownOption,
|
||||||
index?: number,
|
index?: number,
|
||||||
): void => {
|
): void => {
|
||||||
btn.children[index].onCommandClick(event, container);
|
btn.children[index].onCommandClick(event);
|
||||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,17 +237,14 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createKeyboardHandlers(
|
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
|
||||||
allButtons: CommandButtonComponentProps[],
|
|
||||||
container: Explorer,
|
|
||||||
): KeyboardHandlerMap {
|
|
||||||
const handlers: KeyboardHandlerMap = {};
|
const handlers: KeyboardHandlerMap = {};
|
||||||
|
|
||||||
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
if (!button.disabled && button.keyboardAction) {
|
if (!button.disabled && button.keyboardAction) {
|
||||||
handlers[button.keyboardAction] = (e) => {
|
handlers[button.keyboardAction] = (e) => {
|
||||||
button.onCommandClick(e, container);
|
button.onCommandClick(e);
|
||||||
|
|
||||||
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
|
||||||
import create, { UseStore } from "zustand";
|
|
||||||
|
|
||||||
export interface CommandBarStore {
|
|
||||||
contextButtons: CommandButtonComponentProps[];
|
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
|
||||||
isHidden: boolean;
|
|
||||||
setIsHidden: (isHidden: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
|
||||||
contextButtons: [],
|
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
|
||||||
isHidden: false,
|
|
||||||
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
|
||||||
}));
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
import {
|
|
||||||
makeStyles,
|
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuItem,
|
|
||||||
MenuList,
|
|
||||||
MenuPopover,
|
|
||||||
MenuTrigger,
|
|
||||||
Toolbar,
|
|
||||||
ToolbarButton,
|
|
||||||
ToolbarDivider,
|
|
||||||
ToolbarGroup,
|
|
||||||
Tooltip,
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import {
|
|
||||||
createPlatformButtons,
|
|
||||||
createStaticCommandBarButtons,
|
|
||||||
} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory";
|
|
||||||
import { createKeyboardHandlers } from "Explorer/Menus/CommandBar/CommandBarUtil";
|
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
|
||||||
import React, { MouseEventHandler } from "react";
|
|
||||||
|
|
||||||
const useToolbarStyles = makeStyles({
|
|
||||||
toolbar: {
|
|
||||||
height: tokens.layoutRowHeight,
|
|
||||||
justifyContent: "space-between", // Ensures that the two toolbar groups are at opposite ends of the toolbar
|
|
||||||
...cosmosShorthands.borderBottom(),
|
|
||||||
},
|
|
||||||
toolbarGroup: {
|
|
||||||
display: "flex",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface CommandBarV2Props {
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommandBarV2: React.FC<CommandBarV2Props> = ({ explorer }: CommandBarV2Props) => {
|
|
||||||
const styles = useToolbarStyles();
|
|
||||||
const selectedNodeState = useSelectedNode();
|
|
||||||
const contextButtons = useCommandBar((state) => state.contextButtons);
|
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
|
||||||
const staticButtons = createStaticCommandBarButtons(selectedNodeState);
|
|
||||||
const platformButtons = createPlatformButtons();
|
|
||||||
|
|
||||||
if (isHidden) {
|
|
||||||
setKeyboardHandlers({});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
|
|
||||||
const keyboardHandlers = createKeyboardHandlers(allButtons, explorer);
|
|
||||||
setKeyboardHandlers(keyboardHandlers);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CosmosFluentProvider>
|
|
||||||
<Toolbar className={styles.toolbar}>
|
|
||||||
<ToolbarGroup role="presentation" className={styles.toolbarGroup}>
|
|
||||||
{staticButtons.map((button, index) =>
|
|
||||||
renderButton(explorer, button, `static-${index}`, contextButtons?.length > 0),
|
|
||||||
)}
|
|
||||||
{staticButtons.length > 0 && contextButtons?.length > 0 && <ToolbarDivider />}
|
|
||||||
{contextButtons.map((button, index) => renderButton(explorer, button, `context-${index}`, false))}
|
|
||||||
</ToolbarGroup>
|
|
||||||
<ToolbarGroup role="presentation">
|
|
||||||
{platformButtons.map((button, index) => renderButton(explorer, button, `platform-${index}`, true))}
|
|
||||||
</ToolbarGroup>
|
|
||||||
</Toolbar>
|
|
||||||
</CosmosFluentProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// This allows us to migrate individual buttons over to using a JSX.Element for the icon, without requiring us to change them all at once.
|
|
||||||
function renderIcon(iconSrcOrElement: string | JSX.Element, alt?: string): JSX.Element {
|
|
||||||
if (typeof iconSrcOrElement === "string") {
|
|
||||||
return <img src={iconSrcOrElement} alt={alt} />;
|
|
||||||
}
|
|
||||||
return iconSrcOrElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderButton(
|
|
||||||
explorer: Explorer,
|
|
||||||
btn: CommandButtonComponentProps,
|
|
||||||
key: string,
|
|
||||||
iconOnly: boolean,
|
|
||||||
): JSX.Element {
|
|
||||||
if (btn.isDivider) {
|
|
||||||
return <ToolbarDivider key={key} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasChildren = !!btn.children && btn.children.length > 0;
|
|
||||||
const label = btn.commandButtonLabel || btn.tooltipText;
|
|
||||||
const tooltip = btn.tooltipText || (iconOnly ? label : undefined);
|
|
||||||
const onClick: MouseEventHandler | undefined =
|
|
||||||
btn.onCommandClick && !hasChildren ? (e) => btn.onCommandClick(e, explorer) : undefined;
|
|
||||||
|
|
||||||
// We don't know which element will be the top-level element, so just slap a key on all of 'em
|
|
||||||
|
|
||||||
let button = hasChildren ? (
|
|
||||||
<MenuButton key={key} appearance="subtle" aria-label={btn.ariaLabel} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
|
||||||
{!iconOnly && label}
|
|
||||||
</MenuButton>
|
|
||||||
) : (
|
|
||||||
<ToolbarButton key={key} aria-label={btn.ariaLabel} onClick={onClick} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
|
||||||
{!iconOnly && label}
|
|
||||||
</ToolbarButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tooltip) {
|
|
||||||
button = (
|
|
||||||
<Tooltip key={key} content={tooltip} relationship="description" withArrow>
|
|
||||||
{button}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChildren) {
|
|
||||||
button = (
|
|
||||||
<Menu key={key}>
|
|
||||||
<MenuTrigger disableButtonEnhancement>{button}</MenuTrigger>
|
|
||||||
<MenuPopover>
|
|
||||||
<MenuList>{btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))}</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMenuItem(explorer: Explorer, btn: CommandButtonComponentProps, key: string): JSX.Element {
|
|
||||||
const hasChildren = !!btn.children && btn.children.length > 0;
|
|
||||||
const onClick: MouseEventHandler | undefined = btn.onCommandClick
|
|
||||||
? (e) => btn.onCommandClick(e, explorer)
|
|
||||||
: undefined;
|
|
||||||
const item = (
|
|
||||||
<MenuItem key={key} onClick={onClick} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
|
||||||
{btn.commandButtonLabel || btn.tooltipText}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasChildren) {
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<MenuTrigger disableButtonEnhancement>{item}</MenuTrigger>
|
|
||||||
<MenuPopover>
|
|
||||||
<MenuList>{btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))}</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
@@ -37,6 +37,10 @@
|
|||||||
background-color:@NotificationHigh;
|
background-color:@NotificationHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
.focusedBorder();
|
||||||
|
}
|
||||||
|
|
||||||
.statusBar {
|
.statusBar {
|
||||||
.dataTypeIcons {
|
.dataTypeIcons {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -81,10 +81,6 @@ 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,
|
||||||
@@ -101,7 +97,9 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
<div
|
<div
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
ref={this.setElememntRef}
|
role="button"
|
||||||
|
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}
|
||||||
@@ -109,15 +107,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>
|
||||||
@@ -129,17 +127,10 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
|
||||||
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 ? "ChevronDownIcon" : "ChevronUpIcon"}
|
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -259,9 +250,6 @@ 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,10 +5,13 @@ 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
|
||||||
@@ -21,7 +24,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
|
||||||
@@ -34,7 +37,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="error items"
|
alt="Error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -47,7 +50,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="info items"
|
alt="Info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -71,15 +74,11 @@ 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="ChevronUpIcon"
|
alt="Expand icon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,10 +175,13 @@ 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
|
||||||
@@ -192,7 +194,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
|
||||||
@@ -205,7 +207,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="error items"
|
alt="Error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -218,7 +220,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="info items"
|
alt="Info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -244,15 +246,11 @@ 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="ChevronUpIcon"
|
alt="Expand icon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
@@ -128,9 +127,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||||
? databaseAccount?.location
|
? databaseAccount?.location
|
||||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||||
const disallowedLocationsUri: string = useNewPortalBackendEndpoint(Constants.BackendApi.DisallowedLocations)
|
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
|
||||||
? `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`
|
|
||||||
: `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(disallowedLocationsUri, {
|
const response = await fetch(disallowedLocationsUri, {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// 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";
|
||||||
@@ -56,6 +57,19 @@ 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?.();
|
||||||
@@ -121,10 +135,28 @@ function openCollectionTab(
|
|||||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||||
) {
|
) {
|
||||||
|
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||||
collection.onNewQueryClick(
|
collection.onNewQueryClick(
|
||||||
collection,
|
collection,
|
||||||
undefined,
|
undefined,
|
||||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
|
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
||||||
|
openQueryTabAction.splitterDirection,
|
||||||
|
openQueryTabAction.queryViewSizePercent,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
action.tabKind === ActionContracts.TabKind.MongoQuery ||
|
||||||
|
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoQuery]
|
||||||
|
) {
|
||||||
|
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||||
|
collection.onNewMongoQueryClick(
|
||||||
|
collection,
|
||||||
|
undefined,
|
||||||
|
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
||||||
|
openQueryTabAction.splitterDirection,
|
||||||
|
openQueryTabAction.queryViewSizePercent,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
@@ -17,30 +17,54 @@ import {
|
|||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { createCollection } from "Common/dataAccess/createCollection";
|
import { createCollection } from "Common/dataAccess/createCollection";
|
||||||
|
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import { configContext, Platform } from "ConfigContext";
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import * as DataModels from "Contracts/DataModels";
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { SubscriptionType } from "Contracts/SubscriptionType";
|
import { 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 { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import {
|
||||||
|
isCapabilityEnabled,
|
||||||
|
isFullTextSearchEnabled,
|
||||||
|
isServerlessAccount,
|
||||||
|
isVectorSearchEnabled,
|
||||||
|
} from "Utils/CapabilityUtils";
|
||||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||||
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;
|
||||||
@@ -48,40 +72,6 @@ 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: [],
|
||||||
};
|
};
|
||||||
@@ -109,6 +99,9 @@ export interface AddCollectionPanelState {
|
|||||||
vectorIndexingPolicy: DataModels.VectorIndex[];
|
vectorIndexingPolicy: DataModels.VectorIndex[];
|
||||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
||||||
vectorPolicyValidated: boolean;
|
vectorPolicyValidated: boolean;
|
||||||
|
fullTextPolicy: DataModels.FullTextPolicy;
|
||||||
|
fullTextIndexes: DataModels.FullTextIndex[];
|
||||||
|
fullTextPolicyValidated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||||
@@ -125,13 +118,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
createNewDatabase:
|
createNewDatabase:
|
||||||
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
||||||
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
||||||
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
isSharedThroughputChecked: getNewDatabaseSharedThroughputDefault(),
|
||||||
selectedDatabaseId:
|
selectedDatabaseId:
|
||||||
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
||||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||||
enableIndexing: true,
|
enableIndexing: true,
|
||||||
isSharded: userContext.apiType !== "Tables",
|
isSharded: userContext.apiType !== "Tables",
|
||||||
partitionKey: this.getPartitionKey(),
|
partitionKey: getPartitionKey(props.isQuickstart),
|
||||||
subPartitionKeys: [],
|
subPartitionKeys: [],
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: false,
|
||||||
createMongoWildCardIndex:
|
createMongoWildCardIndex:
|
||||||
@@ -147,6 +140,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
vectorEmbeddingPolicy: [],
|
vectorEmbeddingPolicy: [],
|
||||||
vectorIndexingPolicy: [],
|
vectorIndexingPolicy: [],
|
||||||
vectorPolicyValidated: true,
|
vectorPolicyValidated: true,
|
||||||
|
fullTextPolicy: FullTextPolicyDefault,
|
||||||
|
fullTextIndexes: [],
|
||||||
|
fullTextPolicyValidated: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +154,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) {
|
||||||
this.scrollToSection("panelContainer");
|
scrollToSection("panelContainer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +171,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
{!this.state.errorMessage && isFreeTierAccount() && (
|
||||||
<PanelInfoErrorComponent
|
<PanelInfoErrorComponent
|
||||||
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
||||||
messageType="info"
|
messageType="info"
|
||||||
@@ -269,150 +265,152 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
<Stack hidden={userContext.apiType === "Tables"}>
|
{!(isFabricNative() && this.props.databaseId !== undefined) && (
|
||||||
<Stack horizontal>
|
<Stack hidden={userContext.apiType === "Tables"}>
|
||||||
<span className="mandatoryStar">* </span>
|
<Stack horizontal>
|
||||||
<Text className="panelTextBold" variant="small">
|
<span className="mandatoryStar">* </span>
|
||||||
Database {userContext.apiType === "Mongo" ? "name" : "id"}
|
<Text className="panelTextBold" variant="small">
|
||||||
</Text>
|
Database {userContext.apiType === "Mongo" ? "name" : "id"}
|
||||||
<TooltipHost
|
</Text>
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
<TooltipHost
|
||||||
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
true,
|
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
).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()}.`}
|
||||||
/>
|
>
|
||||||
</TooltipHost>
|
<Icon
|
||||||
</Stack>
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
{configContext.platform !== Platform.Fabric && (
|
|
||||||
<Stack horizontal verticalAlign="center">
|
|
||||||
<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}
|
tabIndex={0}
|
||||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
|
true,
|
||||||
|
).toLocaleLowerCase()}.`}
|
||||||
/>
|
/>
|
||||||
<span className="panelRadioBtnLabel">Create new</span>
|
</TooltipHost>
|
||||||
|
|
||||||
<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>
|
</Stack>
|
||||||
)}
|
|
||||||
|
|
||||||
{this.state.createNewDatabase && (
|
{configContext.platform !== Platform.Fabric && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack horizontal verticalAlign="center">
|
||||||
<input
|
<div role="radiogroup">
|
||||||
name="newDatabaseId"
|
<input
|
||||||
id="newDatabaseId"
|
className="panelRadioBtn"
|
||||||
aria-required
|
checked={this.state.createNewDatabase}
|
||||||
required
|
aria-label="Create new database"
|
||||||
type="text"
|
aria-checked={this.state.createNewDatabase}
|
||||||
autoComplete="off"
|
name="databaseType"
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
type="radio"
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
role="radio"
|
||||||
placeholder="Type a new database id"
|
id="databaseCreateNew"
|
||||||
size={40}
|
tabIndex={0}
|
||||||
className="panelTextField"
|
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||||
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
|
<span className="panelRadioBtnLabel">Create new</span>
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
<input
|
||||||
true,
|
className="panelRadioBtn"
|
||||||
).toLocaleLowerCase()} within the database.`}
|
checked={!this.state.createNewDatabase}
|
||||||
>
|
aria-label="Use existing database"
|
||||||
<Icon
|
aria-checked={!this.state.createNewDatabase}
|
||||||
iconName="Info"
|
name="databaseType"
|
||||||
className="panelInfoIcon"
|
type="radio"
|
||||||
tabIndex={0}
|
role="radio"
|
||||||
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
tabIndex={0}
|
||||||
|
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||||
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">Use existing</span>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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,
|
true,
|
||||||
).toLocaleLowerCase()} within the database.`}
|
).toLocaleLowerCase()} within the database.`}
|
||||||
/>
|
>
|
||||||
</TooltipHost>
|
<Icon
|
||||||
</Stack>
|
iconName="Info"
|
||||||
)}
|
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={this.isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={true}
|
isDatabase={true}
|
||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
isFreeTier={this.isFreeTierAccount()}
|
isFreeTier={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>
|
||||||
@@ -561,17 +559,14 @@ 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">
|
||||||
{this.getPartitionKeyName()}
|
{getPartitionKeyName()}
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost
|
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={this.getPartitionKeyTooltipText()}
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
ariaLabel={this.getPartitionKeyTooltipText()}
|
ariaLabel={getPartitionKeyTooltipText()}
|
||||||
/>
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -585,8 +580,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
required
|
required
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
placeholder={this.getPartitionKeyPlaceHolder()}
|
placeholder={getPartitionKeyPlaceHolder()}
|
||||||
aria-label={this.getPartitionKeyName()}
|
aria-label={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}
|
||||||
@@ -624,8 +619,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={this.getPartitionKeyPlaceHolder(index)}
|
placeholder={getPartitionKeyPlaceHolder(index)}
|
||||||
aria-label={this.getPartitionKeyName()}
|
aria-label={getPartitionKeyName()}
|
||||||
pattern={".*"}
|
pattern={".*"}
|
||||||
title={""}
|
title={""}
|
||||||
value={subPartitionKey}
|
value={subPartitionKey}
|
||||||
@@ -651,7 +646,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{userContext.apiType === "SQL" && (
|
{!isFabricNative() && 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 } }}
|
||||||
@@ -716,10 +711,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{this.shouldShowCollectionThroughputInput() && (
|
{this.shouldShowCollectionThroughputInput() && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={false}
|
isDatabase={false}
|
||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
isFreeTier={this.isFreeTierAccount()}
|
isFreeTier={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)}
|
||||||
@@ -732,29 +727,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{userContext.apiType === "SQL" && (
|
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal>
|
{UniqueKeysHeader()}
|
||||||
<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>
|
||||||
@@ -802,31 +777,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.shouldShowAnalyticalStoreOptions() && (
|
{shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Stack horizontal>
|
<Text className="panelTextBold" variant="small">
|
||||||
<Text className="panelTextBold" variant="small">
|
{AnalyticalStorageContent()}
|
||||||
Analytical store
|
</Text>
|
||||||
</Text>
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={this.getAnalyticalStorageTooltipContent()}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
iconName="Info"
|
|
||||||
className="panelInfoIcon"
|
|
||||||
tabIndex={0}
|
|
||||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
|
||||||
/>
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
<div role="radiogroup">
|
<div role="radiogroup">
|
||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={this.state.enableAnalyticalStore}
|
checked={this.state.enableAnalyticalStore}
|
||||||
disabled={!this.isSynapseLinkEnabled()}
|
disabled={!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"
|
||||||
@@ -841,7 +803,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={!this.state.enableAnalyticalStore}
|
checked={!this.state.enableAnalyticalStore}
|
||||||
disabled={!this.isSynapseLinkEnabled()}
|
disabled={!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"
|
||||||
@@ -855,7 +817,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{!this.isSynapseLinkEnabled() && (
|
{!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{" "}
|
||||||
@@ -863,6 +825,7 @@ 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
|
||||||
@@ -884,15 +847,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
title="Container Vector Policy"
|
title="Container Vector Policy"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
scrollToSection("collapsibleVectorPolicySectionContent");
|
||||||
}}
|
}}
|
||||||
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
|
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
||||||
>
|
>
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||||
<AddVectorEmbeddingPolicyForm
|
<VectorEmbeddingPoliciesComponent
|
||||||
vectorEmbedding={this.state.vectorEmbeddingPolicy}
|
vectorEmbeddings={this.state.vectorEmbeddingPolicy}
|
||||||
vectorIndex={this.state.vectorIndexingPolicy}
|
vectorIndexes={this.state.vectorIndexingPolicy}
|
||||||
onVectorEmbeddingChange={(
|
onVectorEmbeddingChange={(
|
||||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
||||||
vectorIndexingPolicy: DataModels.VectorIndex[],
|
vectorIndexingPolicy: DataModels.VectorIndex[],
|
||||||
@@ -906,13 +869,41 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{userContext.apiType !== "Tables" && (
|
{this.shouldShowFullTextSearchParameters() && (
|
||||||
|
<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);
|
||||||
this.scrollToSection("collapsibleAdvancedSectionContent");
|
scrollToSection("collapsibleAdvancedSectionContent");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||||
@@ -1022,31 +1013,6 @@ 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({
|
||||||
@@ -1134,52 +1100,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return !!selectedDatabase?.offer();
|
return !!selectedDatabase?.offer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private isFreeTierAccount(): boolean {
|
|
||||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSharedThroughputDefault(): boolean {
|
|
||||||
return userContext.subscriptionType !== SubscriptionType.EA && !isServerlessAccount();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFreeTierIndexingText(): string {
|
private getFreeTierIndexingText(): string {
|
||||||
return this.state.enableIndexing
|
return this.state.enableIndexing
|
||||||
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
||||||
: "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 &&
|
||||||
@@ -1191,32 +1117,21 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticalStorageTooltipContent(): JSX.Element {
|
//TODO: uncomment when learn more text becomes available
|
||||||
return (
|
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
|
||||||
<Text variant="small">
|
// return (
|
||||||
Enable analytical store capability to perform near real-time analytics on your operational data, without
|
// <Text variant="small">
|
||||||
impacting the performance of transactional workloads.{" "}
|
// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||||
<Link target="_blank" href="https://aka.ms/analytical-store-overview">
|
// magna aliqua.{" "}
|
||||||
Learn more
|
// <Link target="_blank" href="https://aka.ms/CosmosFullTextSearch">
|
||||||
</Link>
|
// Learn more
|
||||||
</Text>
|
// </Link>
|
||||||
);
|
// </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 (isServerlessAccount()) {
|
if (isFabricNative() || isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1232,7 +1147,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
||||||
if (!this.isFreeTierAccount()) {
|
if (!isFreeTierAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1241,43 +1156,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
: this.isSelectedDatabaseSharedThroughput();
|
: this.isSelectedDatabaseSharedThroughput();
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowAnalyticalStoreOptions(): boolean {
|
|
||||||
if (configContext.platform === Platform.Emulator) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (userContext.apiType) {
|
|
||||||
case "SQL":
|
|
||||||
case "Mongo":
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldShowVectorSearchParameters() {
|
private shouldShowVectorSearchParameters() {
|
||||||
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldShowFullTextSearchParameters() {
|
||||||
|
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||||
|
}
|
||||||
|
|
||||||
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
||||||
if (this.state.uniqueKeys?.length === 0) {
|
if (this.state.uniqueKeys?.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -1334,20 +1220,27 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
|
if (this.shouldShowVectorSearchParameters()) {
|
||||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
if (!this.state.vectorPolicyValidated) {
|
||||||
return false;
|
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.fullTextPolicyValidated) {
|
||||||
|
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticalStorageTtl(): number {
|
private getAnalyticalStorageTtl(): number {
|
||||||
if (!this.isSynapseLinkEnabled()) {
|
if (!isSynapseLinkEnabled()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.shouldShowAnalyticalStoreOptions()) {
|
if (!shouldShowAnalyticalStoreOptions()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1361,10 +1254,6 @@ 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()
|
||||||
@@ -1399,7 +1288,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
partitionKeyString = "/'$pk'";
|
partitionKeyString = "/'$pk'";
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys);
|
||||||
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
||||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||||
? {
|
? {
|
||||||
@@ -1427,6 +1316,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.shouldShowFullTextSearchParameters()) {
|
||||||
|
indexingPolicy.fullTextIndexes = this.state.fullTextIndexes;
|
||||||
|
}
|
||||||
|
|
||||||
const telemetryData = {
|
const telemetryData = {
|
||||||
database: {
|
database: {
|
||||||
id: databaseId,
|
id: databaseId,
|
||||||
@@ -1486,6 +1379,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
||||||
vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy,
|
||||||
|
fullTextPolicy: this.state.fullTextPolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState({ isExecuting: true });
|
this.setState({ isExecuting: true });
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
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: [],
|
||||||
|
};
|
||||||
@@ -106,7 +106,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
horizontal={true}
|
horizontal={true}
|
||||||
>
|
>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={true}
|
checked={false}
|
||||||
label="Share throughput across containers"
|
label="Share throughput across containers"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
@@ -137,14 +137,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
/>
|
/>
|
||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
</Stack>
|
</Stack>
|
||||||
<ThroughputInput
|
|
||||||
isDatabase={true}
|
|
||||||
isSharded={true}
|
|
||||||
onCostAcknowledgeChange={[Function]}
|
|
||||||
setIsAutoscale={[Function]}
|
|
||||||
setIsThroughputCapExceeded={[Function]}
|
|
||||||
setThroughputValue={[Function]}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Separator
|
<Separator
|
||||||
className="panelSeparator"
|
className="panelSeparator"
|
||||||
@@ -263,6 +255,14 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
</CustomizedDefaultButton>
|
</CustomizedDefaultButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<ThroughputInput
|
||||||
|
isDatabase={false}
|
||||||
|
isSharded={true}
|
||||||
|
onCostAcknowledgeChange={[Function]}
|
||||||
|
setIsAutoscale={[Function]}
|
||||||
|
setIsThroughputCapExceeded={[Function]}
|
||||||
|
setThroughputValue={[Function]}
|
||||||
|
/>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
@@ -309,40 +309,24 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
<Stack
|
<Stack
|
||||||
className="panelGroupSpacing"
|
className="panelGroupSpacing"
|
||||||
>
|
>
|
||||||
<Stack
|
<Text
|
||||||
horizontal={true}
|
className="panelTextBold"
|
||||||
|
variant="small"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className="panelTextBold"
|
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
Analytical store
|
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
|
||||||
</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
|
<StyledLinkBase
|
||||||
href="https://aka.ms/analytical-store-overview"
|
aria-label="Learn more about analytical store."
|
||||||
target="_blank"
|
href="https://aka.ms/analytical-store-overview"
|
||||||
>
|
target="_blank"
|
||||||
Learn more
|
>
|
||||||
</StyledLinkBase>
|
Learn more
|
||||||
</Text>
|
</StyledLinkBase>
|
||||||
}
|
</Text>
|
||||||
directionalHint={4}
|
</Text>
|
||||||
>
|
|
||||||
<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"
|
||||||
@@ -400,6 +384,7 @@ 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"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||||
|
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
@@ -48,7 +49,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
|
|
||||||
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
|
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
|
||||||
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
|
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
|
||||||
subscriptionType !== SubscriptionType.EA && !isServerlessAccount(),
|
getNewDatabaseSharedThroughputDefault(),
|
||||||
);
|
);
|
||||||
const [formErrors, setFormErrors] = useState<string>("");
|
const [formErrors, setFormErrors] = useState<string>("");
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
horizontal={true}
|
horizontal={true}
|
||||||
>
|
>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={true}
|
checked={false}
|
||||||
label="Provision throughput"
|
label="Provision throughput"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
@@ -90,14 +90,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<ThroughputInput
|
|
||||||
isDatabase={true}
|
|
||||||
isSharded={true}
|
|
||||||
onCostAcknowledgeChange={[Function]}
|
|
||||||
setIsAutoscale={[Function]}
|
|
||||||
setIsThroughputCapExceeded={[Function]}
|
|
||||||
setThroughputValue={[Function]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user