mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 10:51:30 +00:00
Compare commits
173 Commits
ashleyst/r
...
documentdb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98d7e89712 | ||
|
|
d45e10f5ac | ||
|
|
cfb5db4df6 | ||
|
|
922ca5c523 | ||
|
|
bafe002fa3 | ||
|
|
0817acf404 | ||
|
|
8e2c46301d | ||
|
|
012d043c78 | ||
|
|
3afd74a957 | ||
|
|
0ef4399ba4 | ||
|
|
870863a723 | ||
|
|
e3815734db | ||
|
|
5ea78f9abf | ||
|
|
8a56214ec2 | ||
|
|
e3ae006100 | ||
|
|
589b61afaf | ||
|
|
eb3f6bc93f | ||
|
|
6ec909a97b | ||
|
|
08a51ca6b1 | ||
|
|
30a3b5c7a4 | ||
|
|
f370507a27 | ||
|
|
e0edaf405c | ||
|
|
f8231600d6 | ||
|
|
45c8d70c77 | ||
|
|
70d7ee755b | ||
|
|
0a4aed4f47 | ||
|
|
a7d007e0dd | ||
|
|
5f4a4e5c4c | ||
|
|
1b64827c24 | ||
|
|
a6ae784a45 | ||
|
|
7458107efd | ||
|
|
64533b445f | ||
|
|
d7bdd0032e | ||
|
|
372ac6921f | ||
|
|
c6eda097fc | ||
|
|
05d02f08fa | ||
|
|
ab4f02f74a | ||
|
|
0fc6647627 | ||
|
|
c5ed537109 | ||
|
|
db322ccb59 | ||
|
|
2d7631c358 | ||
|
|
e401c88df6 | ||
|
|
f14b574527 | ||
|
|
45513e5e1b | ||
|
|
15154dfd6a | ||
|
|
7aeb682bea | ||
|
|
35051bace5 | ||
|
|
5fc53a7f89 | ||
|
|
ed83bf47e4 | ||
|
|
d657c4919e | ||
|
|
95d33356c3 | ||
|
|
1081432bbd | ||
|
|
44d815454c | ||
|
|
6d604490d3 | ||
|
|
34edd96c76 | ||
|
|
7c0aae6ffa | ||
|
|
86e8bf3c80 | ||
|
|
e98c9a83b8 | ||
|
|
7d57a90d50 | ||
|
|
0f896f556b | ||
|
|
985c744198 | ||
|
|
2dbec019af | ||
|
|
2fa95a281e | ||
|
|
ea6f3d1579 | ||
|
|
f9b0abdd14 | ||
|
|
10cda21401 | ||
|
|
205355bf55 | ||
|
|
bb66deb3a4 | ||
|
|
fe73d0a1c6 | ||
|
|
e90e1fc581 | ||
|
|
8bcad6e0e0 | ||
|
|
9f3236c29c | ||
|
|
2f858ecf9b | ||
|
|
274c85d2de | ||
|
|
d9436be61b | ||
|
|
6db2536a61 | ||
|
|
714f38a1be | ||
|
|
af4e1d10b4 | ||
|
|
6dbc412fa6 | ||
|
|
3470f56535 | ||
|
|
00ec678569 | ||
|
|
27c9ea7ab6 | ||
|
|
d5fe2d9e9f | ||
|
|
94b1e729d1 | ||
|
|
afdbefe36c | ||
|
|
2cff0fc3ff | ||
|
|
a3bfc89318 | ||
|
|
0666e11d89 | ||
|
|
9bb1d0bace | ||
|
|
af0a890516 | ||
|
|
e3c3a8b1b7 | ||
|
|
0f6c979268 | ||
|
|
32576f50d3 | ||
|
|
10f5a5fbfe | ||
|
|
8eb53674dc | ||
|
|
257256f915 | ||
|
|
41f5401016 | ||
|
|
a4c9a47d4e | ||
|
|
c43132d5c0 | ||
|
|
6ce81099ef | ||
|
|
777e411f4f | ||
|
|
63d4b4f4ef | ||
|
|
eaf9a14e7d | ||
|
|
4b65760a1d | ||
|
|
ced2725476 | ||
|
|
b5d7423849 | ||
|
|
1529303107 | ||
|
|
083bccfda9 | ||
|
|
14c9874e5e | ||
|
|
a04eaff6be | ||
|
|
51a412e2c0 | ||
|
|
3fcbdf6152 | ||
|
|
8da078579e | ||
|
|
4ac41031e6 | ||
|
|
d7923db108 | ||
|
|
0170c9e1cc | ||
|
|
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 |
@@ -23,8 +23,6 @@ src/Common/MongoUtility.ts
|
|||||||
src/Common/NotificationsClientBase.ts
|
src/Common/NotificationsClientBase.ts
|
||||||
src/Common/QueriesClient.ts
|
src/Common/QueriesClient.ts
|
||||||
src/Common/Splitter.ts
|
src/Common/Splitter.ts
|
||||||
src/Controls/Heatmap/Heatmap.test.ts
|
|
||||||
src/Controls/Heatmap/Heatmap.ts
|
|
||||||
src/Definitions/datatables.d.ts
|
src/Definitions/datatables.d.ts
|
||||||
src/Definitions/gif.d.ts
|
src/Definitions/gif.d.ts
|
||||||
src/Definitions/globals.d.ts
|
src/Definitions/globals.d.ts
|
||||||
|
|||||||
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)
|
||||||
|
|||||||
139
.github/workflows/ci.yml
vendored
139
.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 }})"
|
||||||
@@ -163,31 +164,49 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
|
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||||
shardTotal: [8]
|
shardTotal: [16]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: "Az CLI login"
|
|
||||||
uses: azure/login@v1
|
|
||||||
with:
|
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npx playwright install --with-deps
|
- run: npx playwright install --with-deps
|
||||||
|
- name: "Az CLI login"
|
||||||
|
uses: Azure/login@v2
|
||||||
|
with:
|
||||||
|
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||||
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
|
||||||
|
- name: "Acquire RBAC tokens for test accounts"
|
||||||
|
uses: azure/cli@v2
|
||||||
|
with:
|
||||||
|
azcliversion: latest
|
||||||
|
inlineScript: |
|
||||||
|
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
|
||||||
|
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
||||||
|
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
||||||
|
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
|
||||||
|
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
- name: Upload blob report to GitHub Actions Artifacts
|
||||||
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 +216,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
|
||||||
|
|||||||
2
.github/workflows/cleanup.yml
vendored
2
.github/workflows/cleanup.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: "Az CLI login"
|
- name: "Az CLI login"
|
||||||
uses: azure/login@v1
|
uses: azure/login@v1
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
|
|||||||
2
.npmrc
2
.npmrc
@@ -1,4 +1,4 @@
|
|||||||
save-exact=true
|
save-exact=true
|
||||||
|
|
||||||
# Ignore peer dependency conflicts
|
# Ignore peer dependency conflicts
|
||||||
force=true # TODO: Remove this when we update to React 17 or higher!
|
force=true # TODO: Remove this when we update to React 17 or higher!
|
||||||
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||||
"isTerminalEnabled": true,
|
|
||||||
"isPhoenixEnabled": true
|
"isPhoenixEnabled": true
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||||
"isTerminalEnabled" : false,
|
"isPhoenixEnabled": false
|
||||||
"isPhoenixEnabled" : false
|
}
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
8
images/VisualStudio.svg
Normal file
8
images/VisualStudio.svg
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 2.1 KiB |
8
images/golang.svg
Normal file
8
images/golang.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g fill="none">
|
||||||
|
<path fill="#8CC5E7" d="M21.4679537,3.20617761 C22.1814672,4.67953668 20.0131274,4.83706564 20.1243243,5.49498069 C20.3281853,6.68108108 20.1891892,8.44169884 20.0316602,10.1745174 C19.7629344,13.1119691 21.9590734,20.1451737 17.3814672,22.9714286 C16.5196911,23.5088803 14.4718147,23.8054054 12.4517375,23.8517375 C12.4517375,23.8517375 12.442471,23.8517375 12.442471,23.8517375 C12.442471,23.8517375 12.4332046,23.8517375 12.4332046,23.8517375 C10.4131274,23.8054054 8.08725869,23.5088803 7.22548263,22.9714286 C2.65714286,20.1451737 4.85328185,13.1119691 4.59382239,10.1745174 C4.42702703,8.44169884 4.28803089,6.68108108 4.5011583,5.49498069 C4.61235521,4.83706564 2.44401544,4.68880309 3.15752896,3.20617761 C3.76911197,1.93667954 5.27953668,3.05791506 5.65945946,2.65945946 C7.596139,0.648648649 9.94980695,0.111196911 11.8030888,0.0648648649 C11.988417,0.0648648649 12.8223938,0.0648648649 12.8223938,0.0648648649 C14.6664093,0.157528958 17.0200772,0.657915058 18.9660232,2.65945946 C19.3459459,3.05791506 20.8471042,1.93667954 21.4679537,3.20617761 Z M11.4324324,10.9065637 C11.3490347,10.9436293 11.2100386,11.8517375 11.6362934,11.8980695 C11.9235521,11.9258687 12.7111969,12.0185328 12.8965251,11.8980695 C13.2579151,11.6664093 13.2208494,11.1104247 13.0169884,10.9714286 C12.6741313,10.7490347 11.5250965,10.8602317 11.4324324,10.9065637 Z M9.07876448,4.10501931 C8.12432432,3.99382239 6.52123552,4.88339768 6.28030888,6.77374517 C6.02084942,8.73822394 8.33745174,10.6841699 10.56139,8.73822394 C11.7567568,7.69111969 12.1737452,4.46640927 9.07876448,4.10501931 Z M15.5281853,4.10501931 C12.4332046,4.46640927 12.8501931,7.69111969 14.0455598,8.73822394 C16.2694981,10.6841699 18.5861004,8.73822394 18.3266409,6.77374517 C18.0949807,4.88339768 16.4918919,3.99382239 15.5281853,4.10501931 Z"/>
|
||||||
|
<path fill="#B8937F" d="M12.3127413,8.98841699 C12.8965251,8.90501931 14.2957529,9.57220077 14.2030888,10.3598456 C14.0918919,11.2772201 10.5984556,11.3976834 10.4131274,10.3042471 C10.3019305,9.63706564 10.8301158,9.21081081 12.3127413,8.98841699 Z M20.1984556,16.3737452 C19.9111969,16.3644788 19.7258687,15.984556 19.7258687,15.7528958 C19.7258687,15.3359073 19.7814672,14.8447876 20.0872587,14.6316602 C20.7173745,14.196139 21.2177606,16.3830116 20.1984556,16.3737452 Z M4.41776062,16.3737452 C3.3984556,16.3830116 3.8988417,14.196139 4.52895753,14.6316602 C4.83474903,14.8447876 4.89034749,15.3359073 4.89034749,15.7528958 C4.89034749,15.984556 4.70501931,16.3644788 4.41776062,16.3737452 Z M18.2617761,23.0918919 C18.4471042,23.3606178 18.4563707,23.5459459 18.1598456,23.6849421 C17.0293436,24.203861 16.019305,23.5088803 16.3992278,23.3142857 C17.2054054,22.9065637 17.7057915,22.2671815 18.2617761,23.0918919 Z M6.35444015,23.184556 C6.91042471,22.3598456 7.41081081,22.9992278 8.21698842,23.4069498 C8.5969112,23.6015444 7.58687259,24.2965251 6.45637066,23.7776062 C6.15984556,23.63861 6.16911197,23.4532819 6.35444015,23.184556 Z"/>
|
||||||
|
<path fill="#000000" d="M19.7351351,3.42857143 C19.7814672,3.23397683 20.2633205,3.14131274 20.5320463,3.47490347 C20.8563707,3.87335907 20.0594595,4.42007722 20.0223938,4.1976834 C19.9297297,3.5953668 19.6795367,3.62316602 19.7351351,3.42857143 Z M4.88108108,3.42857143 C4.93667954,3.62316602 4.68648649,3.5953668 4.59382239,4.1976834 C4.55675676,4.42007722 3.75984556,3.87335907 4.08416988,3.47490347 C4.34362934,3.14131274 4.82548263,3.23397683 4.88108108,3.42857143 Z M15.7413127,7.94131274 C15.1578953,7.94131274 14.6849421,7.46835949 14.6849421,6.88494208 C14.6849421,6.30152468 15.1578953,5.82857143 15.7413127,5.82857143 C16.3247301,5.82857143 16.7976834,6.30152468 16.7976834,6.88494208 C16.7976834,7.46835949 16.3247301,7.94131274 15.7413127,7.94131274 Z M15.4633205,6.76447876 C15.6475575,6.76447876 15.7969112,6.61512511 15.7969112,6.43088803 C15.7969112,6.24665096 15.6475575,6.0972973 15.4633205,6.0972973 C15.2790834,6.0972973 15.1297297,6.24665096 15.1297297,6.43088803 C15.1297297,6.61512511 15.2790834,6.76447876 15.4633205,6.76447876 Z M11.3583012,9.43320463 C11.4694981,9.00694981 11.8586873,8.86795367 12.1737452,8.85868726 C12.9799228,8.84015444 13.2857143,9.27567568 13.3135135,9.61853282 C13.369112,10.2023166 11.1081081,10.3413127 11.3583012,9.43320463 Z M8.87490347,7.94131274 C8.29148607,7.94131274 7.81853282,7.46835949 7.81853282,6.88494208 C7.81853282,6.30152468 8.29148607,5.82857143 8.87490347,5.82857143 C9.45832088,5.82857143 9.93127413,6.30152468 9.93127413,6.88494208 C9.93127413,7.46835949 9.45832088,7.94131274 8.87490347,7.94131274 Z M9.15289575,6.76447876 C9.33713283,6.76447876 9.48648649,6.61512511 9.48648649,6.43088803 C9.48648649,6.24665096 9.33713283,6.0972973 9.15289575,6.0972973 C8.96865868,6.0972973 8.81930502,6.24665096 8.81930502,6.43088803 C8.81930502,6.61512511 8.96865868,6.76447876 9.15289575,6.76447876 Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
10
images/springboot.svg
Normal file
10
images/springboot.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<g>
|
||||||
|
<path d="M38.9437824,35.879008 C89.5234256,-13.1200214 170.398168,-11.8028432 219.397197,39.0402357 C224.929346,31.6640377 229.671187,23.4975328 233.095851,15.0675923 C249.165425,64.0666217 258.912543,105.162582 255.224444,137.038295 C253.380395,163.90873 242.842969,189.725423 225.456217,210.273403 C180.145286,264.014274 99.53398,270.863601 45.7931091,225.55267 L45.7931091,225.55267 L44.765,224.638 L44.7103323,224.601984 C44.5420247,224.484832 44.376007,224.362668 44.2124952,224.235492 C43.7219599,223.853965 43.2765312,223.438607 42.8762093,222.995252 L42.732,222.831 L41.0512675,221.3377 C39.4121124,219.93271 37.7729573,218.52772 36.3188215,216.93771 L35.7825547,216.332423 C-13.2164747,165.752779 -11.6358609,84.8780374 38.9437824,35.879008 Z M57.9111486,207.375611 C53.169307,203.687512 46.3199803,204.214383 42.6318814,208.956225 C39.3888978,213.125775 39.4048731,218.924805 42.6798072,222.771269 L42.732,222.831 L44.765,224.638 L44.9644841,224.773953 C49.5691585,227.80174 55.7644273,227.175885 59.2982065,222.896387 L59.4917624,222.654878 C63.1798614,217.913037 62.3895545,211.06371 57.9111486,207.375611 Z M231.778672,28.2393744 C218.60689,55.9001168 185.940871,76.9749681 157.753257,83.5608592 C131.146257,89.8833146 107.963921,84.6146018 83.4644059,94.0982849 C27.6160498,115.436572 28.6697923,181.822354 59.2283268,196.838185 L59.2283268,196.838185 L61.0723763,197.891928 C61.0723763,197.891928 83.1456487,193.50309 104.973663,187.707242 L106.843514,187.207079 C115.561826,184.857554 124.138869,182.296538 131.146257,179.714869 C167.500376,166.279651 207.542593,133.08676 220.714375,94.6251562 C213.865049,134.667374 179.35498,173.392413 144.84491,191.042601 C126.404416,200.526284 112.178891,202.633769 81.883792,213.171195 C78.195693,214.488373 75.297901,215.805551 75.297901,215.805551 C75.6675607,215.754564 76.0372203,215.70481 76.4060145,215.65629 L77.1421925,215.560893 L77.1421925,215.560893 L77.8745239,215.468787 C84.5652297,214.639554 90.5771682,214.224938 90.5771682,214.224938 C133.517178,212.117452 200.956702,226.342977 232.305544,184.45671 C264.444692,141.780136 246.531068,72.7599979 231.778672,28.2393744 Z" fill="#6DB33F">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M57.9111486,207.375611 C62.3895545,211.06371 63.1798614,217.913037 59.4917624,222.654878 C55.8036635,227.39672 48.9543368,227.923591 44.2124952,224.235492 C39.4706537,220.547393 38.9437824,213.698066 42.6318814,208.956225 C46.3199803,204.214383 53.169307,203.687512 57.9111486,207.375611 Z M231.778672,28.2393744 C246.531068,72.7599979 264.444692,141.780136 232.305544,184.45671 C200.956702,226.342977 133.517178,212.117452 90.5771682,214.224938 C90.5771682,214.224938 84.5652297,214.639554 77.8745239,215.468787 L77.1421925,215.560893 C76.5300999,215.63902 75.9140004,215.720572 75.297901,215.805551 C75.297901,215.805551 78.195693,214.488373 81.883792,213.171195 C112.178891,202.633769 126.404416,200.526284 144.84491,191.042601 C179.35498,173.392413 213.865049,134.667374 220.714375,94.6251562 C207.542593,133.08676 167.500376,166.279651 131.146257,179.714869 C106.119871,188.935116 61.0723763,197.891928 61.0723763,197.891928 L59.2283268,196.838185 C28.6697923,181.822354 27.6160498,115.436572 83.4644059,94.0982849 C107.963921,84.6146018 131.146257,89.8833146 157.753257,83.5608592 C185.940871,76.9749681 218.60689,55.9001168 231.778672,28.2393744 Z" fill="#FFFFFF">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
1
images/vscode.svg
Normal file
1
images/vscode.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="15" height="15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><defs><clipPath id="clip0"><rect x="479" y="279" width="15" height="15"/></clipPath><clipPath id="clip1"><rect x="-0.287396" y="-0.171573" width="152381" height="152381"/></clipPath><image width="35" height="35" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAMAAAApB0NrAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAH4UExURQAAAASExCGs7CC//wB9vQB5uxSU1yaz8ySy9AB8uwB8vAB5uxOR1Say8yax8iaw8gB6vAB6uwB7uwB4uROQ1Cax8ySy8QCAvwB3ugB5ugB2uROP1CWw8yav8wCT2QCQ1QCP1wB2uQB1uBOO0yWv8yat8wCP1QCP1QCP1QCO1QCQ1wB1tQB2uAB3uAB1tx+k6SSt8ySt8QCN0wCO1ACM0wBzuQBztAB1twB0tgB0tiSr8SSs8gCM1ACM1ACEywBwtQBxtAB0tgBztQB0sySp8SSr8gCL0wCK0gCL0wCHzwByuABusgBztQBttiOq8gB6wQCI0QCJ0gB7wySn8SOo8gBxtABtsABwtgCFzwCI0gCG0QCJ0SKn8SKn8iKl8QBvsABvsgBvsQBtsABssgCCzACG0QCF0ACDzyKm8QBtrwBusABsrgCAzQCE0ACF0ACF0ACE0CKj8SKl8QBssQBtrwBtrwBsrgBsrwCK1QCBzwCD0ACA0B2d7CGj8QBsrABrrQBwrwCA3wCC0ACCzwCBzwB+zhGM3iGi8SKh8QCAzQCAzgCAzwB9zRCK3iCh8SGh8SCf8QB+zAB/zgB8zRCJ3SCg8SCf7wB+zQB6zBCI3CCe8CCf8CCe8CCf8SCf/wB7zAB4yxGJ3h6c8CCd7yCf7wmA0RqX60C//5CaUeMAAACodFJOUwA8XAho+//ncID/////53iM//////9wCKv/////gCiYILf///+AMPP/70wYw/+3lP+AXP+MKNv/+3uA/3z/+////+NAgP9c9/////+7HP+A///MgP9Y+////7scgP+AeP/////7/+NA/2D/iyTb//t4gP808//vUBjD/7eU/yifIAi3//////+Ar////////4CI/////3B//////+9/CGj7/+dwEDhYBCm1XqwAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAFpSURBVDhPY2AYBaiAkYkZXQgdsLCysXPgV8XJxc3Dy8vHj0eVgKCQsIioqKioGLoMDIhLSEpKSknLyMjIyKLLyckrgJUoCgsLCyspq6ioqKiiKVFT19DUYmDQ1tEFAT19AwMDA0M0NUbGxsbGJqZm5ubm5haWDFbW1tbWVmhqGGxsbW1t7ewdHB2dnBkYXFxdXV1d0NUwuLl7eHh4enn7+DIwMLj4+fn5Yaph8A8IDAwMDBIHsYNDQkJCgtFVMISGhUdERkZGRkUzMDDExMbGxsahK4lPSExKTklNTU1NS2dgiMvIyMhAV5OZlZWVlZ2Tm5eXl5dfwFBYVFRUVIimpriktKycgaGisgoEqmtqa2tr0dUw1NU3gKjGpuaWlpbWtvb29vYOdDUw0NjZ1dXd09vX39c3AV0OASZOmjR5ytSpU6dOQ5dBAtN7ZsycNXvO3HnoEshg/oKFixYvQRdFA0uXLUcXGqEAAH4FV0z+qQbjAAAAAElFTkSuQmCC" preserveAspectRatio="none" id="img2"></image><clipPath id="clip3"><path d="M44291.4 46947.4 187148 46947.4 187148 188823 44291.4 188823Z" fill-rule="evenodd" clip-rule="evenodd"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-479 -279)"><g clip-path="url(#clip1)" transform="matrix(0.000105 0 0 0.000105 479 279)"><g clip-path="url(#clip3)" transform="matrix(1 0 0 1.00692 -44291.4 -47272.4)"><use width="100%" height="100%" xlink:href="#img2" transform="scale(6709.45 6709.45)"></use></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -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;
|
||||||
@@ -1906,13 +1914,20 @@ input::-webkit-calendar-picker-indicator::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
height: 32px;
|
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
|
|
||||||
.nav-tabs {
|
.nav-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin-bottom: -0.5px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
// Override the bootstrap defaults here to align with our layout constants.
|
||||||
|
margin-bottom: 0px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2618,7 +2633,6 @@ a:link {
|
|||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2855,6 +2869,7 @@ a:link {
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
|
min-height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uniqueIndexesContainer {
|
.uniqueIndexesContainer {
|
||||||
@@ -3118,3 +3133,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%);
|
||||||
}
|
}
|
||||||
@@ -216,4 +210,13 @@ a:focus {
|
|||||||
|
|
||||||
.fileImportImg img {
|
.fileImportImg img {
|
||||||
filter: brightness(0) saturate(100%);
|
filter: brightness(0) saturate(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabPanesContainer {
|
||||||
|
overflow: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-container {
|
||||||
|
min-height: 500px;
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
|||||||
1322
less/quickstart.less
1322
less/quickstart.less
File diff suppressed because it is too large
Load Diff
951
package-lock.json
generated
951
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -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.5.0",
|
||||||
"@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",
|
||||||
@@ -47,6 +46,8 @@
|
|||||||
"@types/mkdirp": "1.0.1",
|
"@types/mkdirp": "1.0.1",
|
||||||
"@types/node-fetch": "2.5.7",
|
"@types/node-fetch": "2.5.7",
|
||||||
"@xmldom/xmldom": "0.7.13",
|
"@xmldom/xmldom": "0.7.13",
|
||||||
|
"@xterm/xterm": "5.5.0",
|
||||||
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"allotment": "1.20.2",
|
"allotment": "1.20.2",
|
||||||
"applicationinsights": "1.8.0",
|
"applicationinsights": "1.8.0",
|
||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
@@ -82,7 +83,7 @@
|
|||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.44.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"p-retry": "4.6.2",
|
"p-retry": "6.2.1",
|
||||||
"patch-package": "8.0.0",
|
"patch-package": "8.0.0",
|
||||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||||
"post-robot": "10.0.42",
|
"post-robot": "10.0.42",
|
||||||
@@ -117,7 +118,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 +171,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",
|
||||||
@@ -247,4 +248,4 @@
|
|||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineConfig, devices } from "@playwright/test";
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
*/
|
*/
|
||||||
@@ -29,24 +28,60 @@ export default defineConfig({
|
|||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "chromium",
|
name: "chromium",
|
||||||
use: { ...devices["Desktop Chrome"] },
|
use: {
|
||||||
|
...devices["Desktop Chrome"],
|
||||||
|
launchOptions: {
|
||||||
|
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "firefox",
|
name: "firefox",
|
||||||
use: { ...devices["Desktop Firefox"] },
|
use: {
|
||||||
|
...devices["Desktop Firefox"],
|
||||||
|
launchOptions: {
|
||||||
|
firefoxUserPrefs: {
|
||||||
|
"security.fileuri.strict_origin_policy": false,
|
||||||
|
"network.http.referer.XOriginPolicy": 0,
|
||||||
|
"network.http.referer.trimmingPolicy": 0,
|
||||||
|
"privacy.file_unique_origin": false,
|
||||||
|
"security.csp.enable": false,
|
||||||
|
"network.cors_preflight.allow_client_cert": true,
|
||||||
|
"dom.security.https_first": false,
|
||||||
|
"network.http.cross-origin-embedder-policy": false,
|
||||||
|
"network.http.cross-origin-opener-policy": false,
|
||||||
|
"browser.tabs.remote.useCrossOriginPolicy": false,
|
||||||
|
"browser.tabs.remote.useCORP": false,
|
||||||
|
},
|
||||||
|
args: ["--disable-web-security"],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "webkit",
|
name: "webkit",
|
||||||
use: { ...devices["Desktop Safari"] },
|
use: {
|
||||||
|
...devices["Desktop Safari"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
/* Test against branded browsers. */
|
|
||||||
{
|
{
|
||||||
name: "Google Chrome",
|
name: "Google Chrome",
|
||||||
use: { ...devices["Desktop Chrome"], channel: "chrome" }, // or 'chrome-beta'
|
use: {
|
||||||
|
...devices["Desktop Chrome"],
|
||||||
|
channel: "chrome",
|
||||||
|
launchOptions: {
|
||||||
|
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Microsoft Edge",
|
name: "Microsoft Edge",
|
||||||
use: { ...devices["Desktop Edge"], channel: "msedge" }, // or 'msedge-dev'
|
use: {
|
||||||
|
...devices["Desktop Edge"],
|
||||||
|
channel: "msedge",
|
||||||
|
launchOptions: {
|
||||||
|
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
37099
preview/package-lock.json
generated
37099
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 {
|
||||||
@@ -130,15 +138,6 @@ export enum MongoBackendEndpointType {
|
|||||||
remote,
|
remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BackendApi {
|
|
||||||
public static readonly GenerateToken: string = "GenerateToken";
|
|
||||||
public static readonly PortalSettings: string = "PortalSettings";
|
|
||||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
|
||||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
|
||||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
|
||||||
public static readonly SampleData: string = "SampleData";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
export class PortalBackendEndpoints {
|
||||||
public static readonly Development: string = "https://localhost:7235";
|
public static readonly Development: string = "https://localhost:7235";
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||||
@@ -148,13 +147,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";
|
||||||
@@ -237,6 +248,7 @@ export class Areas {
|
|||||||
public static ShareDialog: string = "Share Access Dialog";
|
public static ShareDialog: string = "Share Access Dialog";
|
||||||
public static Notebook: string = "Notebook";
|
public static Notebook: string = "Notebook";
|
||||||
public static Copilot: string = "Copilot";
|
public static Copilot: string = "Copilot";
|
||||||
|
public static CloudShell: string = "Cloud Shell";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HttpHeaders {
|
export class HttpHeaders {
|
||||||
@@ -505,6 +517,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 GlobalSecondaryIndexLabels {
|
||||||
|
public static readonly NewGlobalSecondaryIndex: string = "New Global Secondary Index";
|
||||||
|
}
|
||||||
|
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";
|
||||||
|
|
||||||
@@ -741,3 +765,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
|
|||||||
|
|
||||||
userPrompt: "find all products",
|
userPrompt: "find all products",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum MongoGuidRepresentation {
|
||||||
|
Standard = "Standard",
|
||||||
|
CSharpLegacy = "CSharpLegacy",
|
||||||
|
JavaLegacy = "JavaLegacy",
|
||||||
|
PythonLegacy = "PythonLegacy",
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
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 { 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,15 +20,14 @@ 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";
|
if (useDataplaneRbacAuthorization(userContext)) {
|
||||||
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} `,
|
||||||
"Explorer/tokenProvider",
|
"Explorer/tokenProvider",
|
||||||
);
|
);
|
||||||
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 +42,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 +54,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 +71,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 **************
|
||||||
@@ -117,7 +124,11 @@ export const endpoint = () => {
|
|||||||
const location = _global.parent ? _global.parent.location : _global.location;
|
const location = _global.parent ? _global.parent.location : _global.location;
|
||||||
return configContext.EMULATOR_ENDPOINT || location.origin;
|
return configContext.EMULATOR_ENDPOINT || location.origin;
|
||||||
}
|
}
|
||||||
return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
|
return (
|
||||||
|
userContext.selectedRegionalEndpoint ||
|
||||||
|
userContext.endpoint ||
|
||||||
|
userContext?.databaseAccount?.properties?.documentEndpoint
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getTokenFromAuthService(
|
export async function getTokenFromAuthService(
|
||||||
@@ -125,10 +136,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 +158,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 +168,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 ||
|
||||||
@@ -216,6 +206,7 @@ export function client(): Cosmos.CosmosClient {
|
|||||||
userAgentSuffix: "Azure Portal",
|
userAgentSuffix: "Azure Portal",
|
||||||
defaultHeaders: _defaultHeaders,
|
defaultHeaders: _defaultHeaders,
|
||||||
connectionPolicy: {
|
connectionPolicy: {
|
||||||
|
enableEndpointDiscovery: !userContext.selectedRegionalEndpoint,
|
||||||
retryOptions: {
|
retryOptions: {
|
||||||
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
|
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
|
||||||
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),
|
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),
|
||||||
|
|||||||
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,6 @@
|
|||||||
|
import { TagNames, WorkloadType } from "Common/Constants";
|
||||||
|
import { Tags } from "Contracts/DataModels";
|
||||||
|
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
function isVirtualNetworkFilterEnabled() {
|
function isVirtualNetworkFilterEnabled() {
|
||||||
@@ -15,3 +18,18 @@ 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 isGlobalSecondaryIndexEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
!isFabric() && 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,5 +1,7 @@
|
|||||||
import { QueryOperationOptions } from "@azure/cosmos";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as Constants from "../Common/Constants";
|
||||||
import { QueryResults } from "../Contracts/ViewModels";
|
import { QueryResults } from "../Contracts/ViewModels";
|
||||||
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
interface QueryResponse {
|
interface QueryResponse {
|
||||||
// [Todo] remove any
|
// [Todo] remove any
|
||||||
@@ -11,17 +13,15 @@ interface QueryResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MinimalQueryIterator {
|
export interface MinimalQueryIterator {
|
||||||
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
|
fetchNext: () => Promise<QueryResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick<QueryIterator<any>, "fetchNext">;
|
// Pick<QueryIterator<any>, "fetchNext">;
|
||||||
|
|
||||||
export function nextPage(
|
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
|
||||||
documentsIterator: MinimalQueryIterator,
|
TelemetryProcessor.traceStart(Action.ExecuteQuery);
|
||||||
firstItemIndex: number,
|
return documentsIterator.fetchNext().then((response) => {
|
||||||
queryOperationOptions?: QueryOperationOptions,
|
TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab });
|
||||||
): Promise<QueryResults> {
|
|
||||||
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
|
|
||||||
const documents = response.resources;
|
const documents = response.resources;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
||||||
|
|||||||
@@ -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,7 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -82,16 +75,18 @@ 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",
|
||||||
|
});
|
||||||
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 +98,7 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -114,16 +109,18 @@ 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",
|
||||||
|
});
|
||||||
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 +132,7 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -146,16 +143,18 @@ 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",
|
||||||
|
});
|
||||||
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 +166,7 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -178,28 +177,19 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
updateDocument(databaseId, collection, documentId, "{}");
|
updateDocument(databaseId, collection, documentId, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
"https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -208,18 +198,20 @@ 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",
|
||||||
|
});
|
||||||
|
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 +223,13 @@ describe("MongoProxyClient", () => {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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 +241,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,14 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
import {
|
import { getMongoGuidRepresentation } from "Shared/StorageUtility";
|
||||||
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 +61,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 +79,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 +117,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("/");
|
||||||
@@ -215,9 +140,12 @@ export function readDocument(
|
|||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
? documentId.partitionKeyProperties?.[0]
|
? documentId.partitionKeyProperties?.[0]
|
||||||
: "",
|
: "",
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -237,61 +165,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 = {
|
||||||
@@ -306,9 +185,12 @@ export function createDocument(
|
|||||||
partitionKey:
|
partitionKey:
|
||||||
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||||
documentContent: JSON.stringify(documentContent),
|
documentContent: JSON.stringify(documentContent),
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createDocument`, {
|
.fetch(`${endpoint}/createDocument`, {
|
||||||
@@ -328,54 +210,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("/");
|
||||||
@@ -395,8 +235,11 @@ export function updateDocument(
|
|||||||
? documentId.partitionKeyProperties?.[0]
|
? documentId.partitionKeyProperties?.[0]
|
||||||
: "",
|
: "",
|
||||||
documentContent,
|
documentContent,
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -417,139 +260,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 +271,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,
|
||||||
@@ -571,8 +284,11 @@ export function deleteDocuments(
|
|||||||
subscriptionID: userContext.subscriptionId,
|
subscriptionID: userContext.subscriptionId,
|
||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseAccountName: databaseAccount.name,
|
databaseAccountName: databaseAccount.name,
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const endpoint = getFeatureEndpointOrDefault("bulkdelete");
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/bulkdelete`, {
|
.fetch(`${endpoint}/bulkdelete`, {
|
||||||
@@ -596,9 +312,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 +332,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 +352,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 +365,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 }),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { monaco } from "Explorer/LazyMonaco";
|
import { monaco } from "Explorer/LazyMonaco";
|
||||||
import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
|
||||||
|
|
||||||
export enum QueryErrorSeverity {
|
export enum QueryErrorSeverity {
|
||||||
Error = "Error",
|
Error = "Error",
|
||||||
@@ -103,20 +102,9 @@ export interface ErrorEnrichment {
|
|||||||
learnMoreUrl?: string;
|
learnMoreUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {
|
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {};
|
||||||
OPERATION_RU_LIMIT_EXCEEDED: (original) => {
|
|
||||||
if (ruThresholdEnabled()) {
|
|
||||||
const threshold = getRUThreshold();
|
|
||||||
return `Query exceeded the Request Unit (RU) limit of ${threshold} RUs. You can change this limit in Data Explorer settings.`;
|
|
||||||
}
|
|
||||||
return original;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const HELP_LINKS: Record<string, string> = {
|
const HELP_LINKS: Record<string, string> = {};
|
||||||
OPERATION_RU_LIMIT_EXCEEDED:
|
|
||||||
"https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer#configure-request-unit-threshold",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class QueryError {
|
export default class QueryError {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -214,16 +202,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">
|
||||||
|
|||||||
@@ -4,13 +4,18 @@ import * as React from "react";
|
|||||||
export interface TooltipProps {
|
export interface TooltipProps {
|
||||||
children: string;
|
children: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
ariaLabelForTooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children, className }: TooltipProps) => {
|
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
ariaLabelForTooltip = children,
|
||||||
|
}: TooltipProps) => {
|
||||||
return (
|
return (
|
||||||
<span className={className}>
|
<span className={className}>
|
||||||
<TooltipHost content={children}>
|
<TooltipHost content={children}>
|
||||||
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
<Icon iconName="Info" aria-label={ariaLabelForTooltip} className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
||||||
{
|
{
|
||||||
"disableNonStreamingOrderByQuery": true,
|
"enableQueryControl": false,
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
"maxDegreeOfParallelism": 0,
|
"maxDegreeOfParallelism": 0,
|
||||||
@@ -13,7 +13,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] =
|
|||||||
|
|
||||||
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
||||||
{
|
{
|
||||||
"disableNonStreamingOrderByQuery": true,
|
"enableQueryControl": false,
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
"maxDegreeOfParallelism": 17,
|
"maxDegreeOfParallelism": 17,
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
74
src/Common/dataAccess/createMaterializedView.ts
Normal file
74
src/Common/dataAccess/createMaterializedView.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { constructRpOptions } from "Common/dataAccess/createCollection";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { Collection, CreateMaterializedViewsParams as CreateGlobalSecondaryIndexParams } 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 createGlobalSecondaryIndex = async (params: CreateGlobalSecondaryIndexParams): Promise<Collection> => {
|
||||||
|
const clearMessage = logConsoleProgress(
|
||||||
|
`Creating a new global secondary index ${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 global secondary index ${params.materializedViewId}`);
|
||||||
|
|
||||||
|
return createResponse && (createResponse.properties.resource as Collection);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(
|
||||||
|
error,
|
||||||
|
"CreateGlobalSecondaryIndex",
|
||||||
|
`Error while creating global secondary index ${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();
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export interface IBulkDeleteResult {
|
|||||||
export const deleteDocuments = async (
|
export const deleteDocuments = async (
|
||||||
collection: CollectionBase,
|
collection: CollectionBase,
|
||||||
documentIds: DocumentId[],
|
documentIds: DocumentId[],
|
||||||
|
abortSignal: AbortSignal,
|
||||||
): Promise<IBulkDeleteResult[]> => {
|
): Promise<IBulkDeleteResult[]> => {
|
||||||
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
||||||
try {
|
try {
|
||||||
@@ -65,12 +66,16 @@ export const deleteDocuments = async (
|
|||||||
operationType: BulkOperationType.Delete,
|
operationType: BulkOperationType.Delete,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
|
const promise = v2Container.items
|
||||||
return bulkResults.map((bulkResult, index) => {
|
.bulk(operations, undefined, {
|
||||||
const documentId = documentIdsChunk[index];
|
abortSignal,
|
||||||
return { ...bulkResult, documentId };
|
})
|
||||||
|
.then((bulkResults) => {
|
||||||
|
return bulkResults.map((bulkResult, index) => {
|
||||||
|
const documentId = documentIdsChunk[index];
|
||||||
|
return { ...bulkResult, documentId };
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
promiseArray.push(promise);
|
promiseArray.push(promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -41,7 +42,7 @@ interface MetricsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||||
if (userContext.authType !== AuthType.AAD) {
|
if (userContext.authType !== AuthType.AAD || isFabricNative()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
|
||||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
import { Queries } from "../Constants";
|
import { Queries } from "../Constants";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
@@ -26,7 +25,7 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
|||||||
options.maxItemCount ||
|
options.maxItemCount ||
|
||||||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
||||||
Queries.itemsPerPage;
|
Queries.itemsPerPage;
|
||||||
|
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
|
||||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||||
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
|
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { QueryOperationOptions } from "@azure/cosmos";
|
|
||||||
import { QueryResults } from "../../Contracts/ViewModels";
|
import { QueryResults } from "../../Contracts/ViewModels";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { getEntityName } from "../DocumentUtility";
|
import { getEntityName } from "../DocumentUtility";
|
||||||
@@ -9,13 +8,12 @@ export const queryDocumentsPage = async (
|
|||||||
resourceName: string,
|
resourceName: string,
|
||||||
documentsIterator: MinimalQueryIterator,
|
documentsIterator: MinimalQueryIterator,
|
||||||
firstItemIndex: number,
|
firstItemIndex: number,
|
||||||
queryOperationOptions?: QueryOperationOptions,
|
|
||||||
): Promise<QueryResults> => {
|
): Promise<QueryResults> => {
|
||||||
const entityName = getEntityName();
|
const entityName = getEntityName();
|
||||||
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions);
|
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
|
||||||
const itemCount = (result.documents && result.documents.length) || 0;
|
const itemCount = (result.documents && result.documents.length) || 0;
|
||||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -13,6 +14,11 @@ import { readOfferWithSDK } from "./readOfferWithSDK";
|
|||||||
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
||||||
|
|
||||||
|
if (isFabric()) {
|
||||||
|
// Not exposing offers in Fabric
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
@@ -105,6 +111,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 +122,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
|
throughputBuckets,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +134,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
|
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||||
import {
|
import {
|
||||||
BackendApi,
|
|
||||||
CassandraProxyEndpoints,
|
|
||||||
JunoEndpoints,
|
|
||||||
MongoProxyEndpoints,
|
|
||||||
PortalBackendEndpoints,
|
|
||||||
} from "Common/Constants";
|
|
||||||
import {
|
|
||||||
allowedAadEndpoints,
|
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedCassandraProxyEndpoints,
|
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMongoBackendEndpoints,
|
|
||||||
allowedMongoProxyEndpoints,
|
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
|
defaultAllowedAadEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
|
defaultAllowedCassandraProxyEndpoints,
|
||||||
|
defaultAllowedGraphEndpoints,
|
||||||
|
defaultAllowedMongoProxyEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
|
|
||||||
@@ -30,16 +23,18 @@ export enum Platform {
|
|||||||
|
|
||||||
export interface ConfigContext {
|
export interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
|
allowedAadEndpoints: ReadonlyArray<string>;
|
||||||
|
allowedGraphEndpoints: ReadonlyArray<string>;
|
||||||
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;
|
||||||
AAD_ENDPOINT: string;
|
AAD_ENDPOINT: string;
|
||||||
ARM_AUTH_AREA: string;
|
|
||||||
ARM_ENDPOINT: string;
|
ARM_ENDPOINT: string;
|
||||||
EMULATOR_ENDPOINT?: string;
|
EMULATOR_ENDPOINT?: string;
|
||||||
ARM_API_VERSION: string;
|
|
||||||
GRAPH_ENDPOINT: string;
|
GRAPH_ENDPOINT: string;
|
||||||
GRAPH_API_VERSION: string;
|
GRAPH_API_VERSION: string;
|
||||||
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
|
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
|
||||||
@@ -48,20 +43,14 @@ 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[];
|
|
||||||
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[];
|
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
GITHUB_CLIENT_ID: string;
|
GITHUB_CLIENT_ID: string;
|
||||||
GITHUB_TEST_ENV_CLIENT_ID: string;
|
GITHUB_TEST_ENV_CLIENT_ID: string;
|
||||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||||
isTerminalEnabled: boolean;
|
|
||||||
isPhoenixEnabled: boolean;
|
isPhoenixEnabled: boolean;
|
||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
@@ -71,28 +60,28 @@ export interface ConfigContext {
|
|||||||
// Default configuration
|
// Default configuration
|
||||||
let configContext: Readonly<ConfigContext> = {
|
let configContext: Readonly<ConfigContext> = {
|
||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
|
allowedAadEndpoints: defaultAllowedAadEndpoints,
|
||||||
|
allowedGraphEndpoints: defaultAllowedGraphEndpoints,
|
||||||
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$`,
|
||||||
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
||||||
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
||||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
|
||||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
|
||||||
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net$`,
|
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||||
ARM_AUTH_AREA: "https://management.azure.com/",
|
|
||||||
ARM_ENDPOINT: "https://management.azure.com/",
|
ARM_ENDPOINT: "https://management.azure.com/",
|
||||||
ARM_API_VERSION: "2016-06-01",
|
|
||||||
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
||||||
GRAPH_API_VERSION: "1.6",
|
GRAPH_API_VERSION: "1.6",
|
||||||
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
||||||
@@ -103,23 +92,9 @@ 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"],
|
|
||||||
isTerminalEnabled: false,
|
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,19 +110,21 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
if (!validateEndpoint(newContext.AAD_ENDPOINT, configContext.allowedAadEndpoints || defaultAllowedAadEndpoints)) {
|
||||||
delete newContext.ARM_ENDPOINT;
|
delete newContext.AAD_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
|
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
||||||
delete newContext.AAD_ENDPOINT;
|
delete newContext.ARM_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
||||||
delete newContext.EMULATOR_ENDPOINT;
|
delete newContext.EMULATOR_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
|
if (
|
||||||
|
!validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints || defaultAllowedGraphEndpoints)
|
||||||
|
) {
|
||||||
delete newContext.GRAPH_ENDPOINT;
|
delete newContext.GRAPH_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,22 +134,28 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!validateEndpoint(
|
!validateEndpoint(
|
||||||
newContext.BACKEND_ENDPOINT,
|
newContext.PORTAL_BACKEND_ENDPOINT,
|
||||||
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
delete newContext.BACKEND_ENDPOINT;
|
delete newContext.PORTAL_BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
|
if (
|
||||||
|
!validateEndpoint(
|
||||||
|
newContext.MONGO_PROXY_ENDPOINT,
|
||||||
|
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
|
||||||
|
)
|
||||||
|
) {
|
||||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,6 +23,7 @@ export enum PaneKind {
|
|||||||
GlobalSettings,
|
GlobalSettings,
|
||||||
AdHocAccess,
|
AdHocAccess,
|
||||||
SwitchDirectory,
|
SwitchDirectory,
|
||||||
|
QuickStart,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +53,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ export type DataExploreMessageV3 =
|
|||||||
| {
|
| {
|
||||||
type: FabricMessageTypes.GetAllResourceTokens;
|
type: FabricMessageTypes.GetAllResourceTokens;
|
||||||
id: string;
|
id: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: FabricMessageTypes.OpenSettings;
|
||||||
|
settingsId: string;
|
||||||
};
|
};
|
||||||
|
export interface GetCosmosTokenMessageOptions {
|
||||||
export type GetCosmosTokenMessageOptions = {
|
|
||||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
||||||
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
|
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export interface ArmEntity {
|
|||||||
location: string;
|
location: string;
|
||||||
type: string;
|
type: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
|
tags?: Tags;
|
||||||
|
resourceGroup?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccount extends ArmEntity {
|
export interface DatabaseAccount extends ArmEntity {
|
||||||
@@ -31,6 +33,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[];
|
||||||
@@ -41,6 +44,12 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
publicNetworkAccess?: string;
|
publicNetworkAccess?: string;
|
||||||
enablePriorityBasedExecution?: boolean;
|
enablePriorityBasedExecution?: boolean;
|
||||||
vcoreMongoEndpoint?: string;
|
vcoreMongoEndpoint?: string;
|
||||||
|
apiProperties?: ApiProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiProperties {
|
||||||
|
/* Describes the version of the MongoDB account. */
|
||||||
|
serverVersion?: "3.2" | "3.6" | "4.0" | "4.2" | "5.0" | "6.0" | "7.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccountResponseLocation {
|
export interface DatabaseAccountResponseLocation {
|
||||||
@@ -159,9 +168,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 +211,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";
|
||||||
|
vectorIndexShardKey?: string[];
|
||||||
|
indexingSearchListSize?: number;
|
||||||
|
quantizationByteSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullTextIndex {
|
||||||
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComputedProperty {
|
export interface ComputedProperty {
|
||||||
@@ -213,6 +233,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 +296,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 +366,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 +377,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 {
|
||||||
@@ -349,12 +395,22 @@ export interface VectorEmbeddingPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorEmbedding {
|
export interface VectorEmbedding {
|
||||||
dataType: "float16" | "float32" | "uint8" | "int8";
|
dataType: "float32" | "uint8" | "int8";
|
||||||
dimensions: number;
|
dimensions: number;
|
||||||
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
||||||
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 +432,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 +700,5 @@ export interface FeatureRegistration {
|
|||||||
state: string;
|
state: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Tags = { [key: string]: string };
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
export enum FabricMessageTypes {
|
export enum FabricMessageTypes {
|
||||||
GetAuthorizationToken = "GetAuthorizationToken",
|
GetAuthorizationToken = "GetAuthorizationToken",
|
||||||
GetAllResourceTokens = "GetAllResourceTokens",
|
GetAllResourceTokens = "GetAllResourceTokens",
|
||||||
|
GetAccessToken = "GetAccessToken",
|
||||||
Ready = "Ready",
|
Ready = "Ready",
|
||||||
|
OpenSettings = "OpenSettings",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthorizationToken {
|
export interface AuthorizationToken {
|
||||||
|
|||||||
@@ -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,88 @@ 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 };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "refreshResourceTree";
|
||||||
|
message: {
|
||||||
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
|
ItemDefinition,
|
||||||
|
JSONObject,
|
||||||
QueryMetrics,
|
QueryMetrics,
|
||||||
Resource,
|
Resource,
|
||||||
StoredProcedureDefinition,
|
StoredProcedureDefinition,
|
||||||
@@ -29,8 +31,11 @@ export interface UploadDetailsRecord {
|
|||||||
numFailed: number;
|
numFailed: number;
|
||||||
numThrottled: number;
|
numThrottled: number;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
|
resources?: ItemDefinition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BulkInsertResult = Omit<UploadDetailsRecord, "fileName">;
|
||||||
|
|
||||||
export interface QueryResultsMetadata {
|
export interface QueryResultsMetadata {
|
||||||
hasMoreResults: boolean;
|
hasMoreResults: boolean;
|
||||||
firstItemIndex: number;
|
firstItemIndex: number;
|
||||||
@@ -45,6 +50,7 @@ export interface QueryResults extends QueryResultsMetadata {
|
|||||||
roundTrips?: number;
|
roundTrips?: number;
|
||||||
headers?: any;
|
headers?: any;
|
||||||
queryMetrics?: QueryMetrics;
|
queryMetrics?: QueryMetrics;
|
||||||
|
ruThresholdExceeded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Button {
|
export interface Button {
|
||||||
@@ -115,7 +121,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 +138,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 +149,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 +165,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;
|
||||||
@@ -190,6 +212,12 @@ export interface Collection extends CollectionBase {
|
|||||||
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
||||||
|
bulkInsertDocuments(documents: JSONObject[]): Promise<{
|
||||||
|
numSucceeded: number;
|
||||||
|
numFailed: number;
|
||||||
|
numThrottled: number;
|
||||||
|
errors: string[];
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -309,6 +337,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 +412,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;
|
||||||
@@ -412,6 +443,7 @@ export interface DataExplorerInputsFrame {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
feedbackPolicies?: any;
|
feedbackPolicies?: any;
|
||||||
|
aadToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelfServeFrameInputs {
|
export interface SelfServeFrameInputs {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html class="no-js" lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="data:," />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="heatmap"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
@import "../../../less/Common/Constants";
|
|
||||||
html {
|
|
||||||
font-family: @DataExplorerFont;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
border: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: @DataExplorerFont;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
border: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#heatmap {
|
|
||||||
.dark-theme {
|
|
||||||
color: @BaseLight;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chartTitle {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
left: 3px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noDataMessage {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10000;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
opacity: 0.97;
|
|
||||||
div {
|
|
||||||
border-color: rgba(204, 204, 204, 0.8);
|
|
||||||
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.12);
|
|
||||||
padding: 15px 10px;
|
|
||||||
width: calc(55% - 40px);
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: center;
|
|
||||||
border-width: 1px;
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { handleMessage, Heatmap, isDarkTheme } from "./Heatmap";
|
|
||||||
import { PortalTheme } from "./HeatmapDatatypes";
|
|
||||||
|
|
||||||
describe("The Heatmap Control", () => {
|
|
||||||
const dataPoints = {
|
|
||||||
"1": {
|
|
||||||
"2019-06-19T00:59:10Z": {
|
|
||||||
"Normalized Throughput": 0.35,
|
|
||||||
},
|
|
||||||
"2019-06-19T00:48:10Z": {
|
|
||||||
"Normalized Throughput": 0.25,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartCaptions = {
|
|
||||||
chartTitle: "chart title",
|
|
||||||
yAxisTitle: "YAxisTitle",
|
|
||||||
tooltipText: "Tooltip text",
|
|
||||||
timeWindow: 123456789,
|
|
||||||
};
|
|
||||||
|
|
||||||
let heatmap: Heatmap;
|
|
||||||
const theme: PortalTheme = 1;
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
|
||||||
|
|
||||||
describe("drawHeatmap rendering", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
heatmap = new Heatmap(dataPoints, chartCaptions, theme);
|
|
||||||
document.body.innerHTML = divElement;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
document.body.innerHTML = ``;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call _getChartSettings when drawHeatmap is invoked", () => {
|
|
||||||
const _getChartSettings = jest.spyOn(heatmap, "_getChartSettings");
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(_getChartSettings).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call _getLayoutSettings when drawHeatmap is invoked", () => {
|
|
||||||
const _getLayoutSettings = jest.spyOn(heatmap, "_getLayoutSettings");
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(_getLayoutSettings).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call _getChartDisplaySettings when drawHeatmap is invoked", () => {
|
|
||||||
const _getChartDisplaySettings = jest.spyOn(heatmap, "_getChartDisplaySettings");
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(_getChartDisplaySettings).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("drawHeatmap should render a Heatmap inside the div element", () => {
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(document.body.innerHTML).not.toEqual(divElement);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("generateMatrixFromMap", () => {
|
|
||||||
it("should massage input data to match output expected", () => {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).yAxisPoints).toEqual(["1"]);
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).dataPoints).toEqual([[0.25, 0.35]]);
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints.length).toEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should output the date format to ISO8601 string format", () => {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(10, 11)).toEqual("T");
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(-1)).toEqual("Z");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should convert the time to the user's local time", () => {
|
|
||||||
if (dayjs().utcOffset()) {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([
|
|
||||||
"2019-06-19T00:48:10Z",
|
|
||||||
"2019-06-19T00:59:10Z",
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([
|
|
||||||
"2019-06-19T00:48:10Z",
|
|
||||||
"2019-06-19T00:59:10Z",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isDarkTheme", () => {
|
|
||||||
it("isDarkTheme should return the correct result", () => {
|
|
||||||
expect(isDarkTheme(PortalTheme.dark)).toEqual(true);
|
|
||||||
expect(isDarkTheme(PortalTheme.azure)).not.toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("iframe rendering when there is no data", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
document.body.innerHTML = ``;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show a no data message with a dark theme", () => {
|
|
||||||
const data = {
|
|
||||||
data: {
|
|
||||||
signature: "pcIframe",
|
|
||||||
data: {
|
|
||||||
chartData: {},
|
|
||||||
chartSettings: {},
|
|
||||||
theme: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
origin: "http://localhost",
|
|
||||||
};
|
|
||||||
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
|
||||||
document.body.innerHTML = divElement;
|
|
||||||
|
|
||||||
handleMessage(data as MessageEvent);
|
|
||||||
expect(document.body.innerHTML).toContain("dark-theme");
|
|
||||||
expect(document.body.innerHTML).toContain("noDataMessage");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show a no data message with a white theme", () => {
|
|
||||||
const data = {
|
|
||||||
data: {
|
|
||||||
signature: "pcIframe",
|
|
||||||
data: {
|
|
||||||
chartData: {},
|
|
||||||
chartSettings: {},
|
|
||||||
theme: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
origin: "http://localhost",
|
|
||||||
};
|
|
||||||
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
|
||||||
document.body.innerHTML = divElement;
|
|
||||||
|
|
||||||
handleMessage(data as MessageEvent);
|
|
||||||
expect(document.body.innerHTML).not.toContain("dark-theme");
|
|
||||||
expect(document.body.innerHTML).toContain("noDataMessage");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import * as Plotly from "plotly.js-cartesian-dist-min";
|
|
||||||
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
|
|
||||||
import { StyleConstants } from "../../Common/StyleConstants";
|
|
||||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
|
||||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
|
||||||
import "./Heatmap.less";
|
|
||||||
import {
|
|
||||||
ChartSettings,
|
|
||||||
DataPayload,
|
|
||||||
DisplaySettings,
|
|
||||||
FontSettings,
|
|
||||||
HeatmapCaptions,
|
|
||||||
HeatmapData,
|
|
||||||
LayoutSettings,
|
|
||||||
PartitionTimeStampToData,
|
|
||||||
PortalTheme,
|
|
||||||
} from "./HeatmapDatatypes";
|
|
||||||
|
|
||||||
export class Heatmap {
|
|
||||||
public static readonly elementId: string = "heatmap";
|
|
||||||
|
|
||||||
private _chartData: HeatmapData;
|
|
||||||
private _heatmapCaptions: HeatmapCaptions;
|
|
||||||
private _theme: PortalTheme;
|
|
||||||
private _defaultFontColor: string;
|
|
||||||
|
|
||||||
constructor(data: DataPayload, heatmapCaptions: HeatmapCaptions, theme: PortalTheme) {
|
|
||||||
this._theme = theme;
|
|
||||||
this._defaultFontColor = StyleConstants.BaseDark;
|
|
||||||
this._setThemeColorForChart();
|
|
||||||
this._chartData = this.generateMatrixFromMap(data);
|
|
||||||
this._heatmapCaptions = heatmapCaptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setThemeColorForChart() {
|
|
||||||
if (isDarkTheme(this._theme)) {
|
|
||||||
this._defaultFontColor = StyleConstants.BaseLight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings {
|
|
||||||
return {
|
|
||||||
family: StyleConstants.DataExplorerFont,
|
|
||||||
size,
|
|
||||||
color,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public generateMatrixFromMap(data: DataPayload): HeatmapData {
|
|
||||||
// all keys in data payload, sorted...
|
|
||||||
const rows: string[] = Object.keys(data).sort((a: string, b: string) => {
|
|
||||||
if (parseInt(a) < parseInt(b)) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
if (parseInt(a) > parseInt(b)) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const output: HeatmapData = {
|
|
||||||
yAxisPoints: [],
|
|
||||||
dataPoints: [],
|
|
||||||
xAxisPoints: Object.keys(data[rows[0]]).sort((a: string, b: string) => {
|
|
||||||
if (a < b) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
if (a > b) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
// go thru all rows and create 2d matrix for heatmap...
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
|
||||||
output.yAxisPoints.push(rows[i]);
|
|
||||||
const dataPoints: number[] = [];
|
|
||||||
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
|
||||||
const row: PartitionTimeStampToData = data[rows[i]];
|
|
||||||
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
|
|
||||||
}
|
|
||||||
output.dataPoints.push(dataPoints);
|
|
||||||
}
|
|
||||||
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
|
||||||
const dateTime = output.xAxisPoints[a];
|
|
||||||
// convert to local users timezone...
|
|
||||||
const day = dayjs(new Date(dateTime)).format("YYYY-MM-DD");
|
|
||||||
const hour = dayjs(new Date(dateTime)).format("HH:mm:ss");
|
|
||||||
// coerce to ISOString format since that is what plotly wants...
|
|
||||||
output.xAxisPoints[a] = `${day}T${hour}Z`;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
// public for testing purposes
|
|
||||||
public _getChartSettings(): ChartSettings[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
z: this._chartData.dataPoints,
|
|
||||||
type: "heatmap",
|
|
||||||
zmin: 0,
|
|
||||||
zmid: 50,
|
|
||||||
zmax: 100,
|
|
||||||
colorscale: [
|
|
||||||
[0.0, "#1FD338"],
|
|
||||||
[0.1, "#1CAD2F"],
|
|
||||||
[0.2, "#50A527"],
|
|
||||||
[0.3, "#719F21"],
|
|
||||||
[0.4, "#95991B"],
|
|
||||||
[0.5, "#CE8F11"],
|
|
||||||
[0.6, "#E27F0F"],
|
|
||||||
[0.7, "#E46612"],
|
|
||||||
[0.8, "#E64914"],
|
|
||||||
[0.9, "#B80016"],
|
|
||||||
[1.0, "#B80016"],
|
|
||||||
],
|
|
||||||
name: "",
|
|
||||||
hovertemplate: this._heatmapCaptions.tooltipText,
|
|
||||||
colorbar: {
|
|
||||||
thickness: 15,
|
|
||||||
outlinewidth: 0,
|
|
||||||
tickcolor: StyleConstants.BaseDark,
|
|
||||||
tickfont: this._getFontStyles(10, this._defaultFontColor),
|
|
||||||
},
|
|
||||||
y: this._chartData.yAxisPoints,
|
|
||||||
x: this._chartData.xAxisPoints,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// public for testing purposes
|
|
||||||
public _getLayoutSettings(): LayoutSettings {
|
|
||||||
return {
|
|
||||||
margin: {
|
|
||||||
l: 40,
|
|
||||||
r: 10,
|
|
||||||
b: 35,
|
|
||||||
t: 30,
|
|
||||||
pad: 0,
|
|
||||||
},
|
|
||||||
paper_bgcolor: "transparent",
|
|
||||||
plot_bgcolor: "transparent",
|
|
||||||
width: 462,
|
|
||||||
height: 240,
|
|
||||||
yaxis: {
|
|
||||||
title: this._heatmapCaptions.yAxisTitle,
|
|
||||||
titlefont: this._getFontStyles(11),
|
|
||||||
autorange: true,
|
|
||||||
showgrid: false,
|
|
||||||
zeroline: false,
|
|
||||||
showline: false,
|
|
||||||
autotick: true,
|
|
||||||
fixedrange: true,
|
|
||||||
ticks: "",
|
|
||||||
showticklabels: false,
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
fixedrange: true,
|
|
||||||
title: "*White area in heatmap indicates there is no available data",
|
|
||||||
titlefont: this._getFontStyles(11),
|
|
||||||
autorange: true,
|
|
||||||
showgrid: false,
|
|
||||||
zeroline: false,
|
|
||||||
showline: false,
|
|
||||||
autotick: true,
|
|
||||||
tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e",
|
|
||||||
showticklabels: true,
|
|
||||||
tickfont: this._getFontStyles(10),
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: this._heatmapCaptions.chartTitle,
|
|
||||||
x: 0.01,
|
|
||||||
font: this._getFontStyles(13, this._defaultFontColor),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// public for testing purposes
|
|
||||||
public _getChartDisplaySettings(): DisplaySettings {
|
|
||||||
return {
|
|
||||||
/* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings
|
|
||||||
responsive: true,*/
|
|
||||||
displayModeBar: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public drawHeatmap(): void {
|
|
||||||
// todo - create random elementId generator so multiple heatmaps can be created - ticket # 431469
|
|
||||||
Plotly.plot(
|
|
||||||
Heatmap.elementId,
|
|
||||||
this._getChartSettings(),
|
|
||||||
this._getLayoutSettings(),
|
|
||||||
this._getChartDisplaySettings(),
|
|
||||||
);
|
|
||||||
const plotDiv: any = document.getElementById(Heatmap.elementId);
|
|
||||||
plotDiv.on("plotly_click", (data: any) => {
|
|
||||||
let timeSelected: string = data.points[0].x;
|
|
||||||
timeSelected = timeSelected.replace(" ", "T");
|
|
||||||
timeSelected = `${timeSelected}Z`;
|
|
||||||
let xAxisIndex = 0;
|
|
||||||
for (let i = 0; i < this._chartData.xAxisPoints.length; i++) {
|
|
||||||
if (this._chartData.xAxisPoints[i] === timeSelected) {
|
|
||||||
xAxisIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const output = [];
|
|
||||||
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
|
|
||||||
output.push(this._chartData.dataPoints[i][xAxisIndex]);
|
|
||||||
}
|
|
||||||
sendCachedDataMessage(MessageTypes.LogInfo, output);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDarkTheme(theme: PortalTheme) {
|
|
||||||
return theme === PortalTheme.dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleMessage(event: MessageEvent) {
|
|
||||||
if (isInvalidParentFrameOrigin(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof event.data.data !== "object" ||
|
|
||||||
!("chartData" in event.data.data) ||
|
|
||||||
!("chartSettings" in event.data.data)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Plotly.purge(Heatmap.elementId);
|
|
||||||
|
|
||||||
document.getElementById(Heatmap.elementId)!.innerHTML = "";
|
|
||||||
const data = event.data.data;
|
|
||||||
const chartData: DataPayload = data.chartData;
|
|
||||||
const chartSettings: HeatmapCaptions = data.chartSettings;
|
|
||||||
const chartTheme: PortalTheme = data.theme;
|
|
||||||
if (Object.keys(chartData).length) {
|
|
||||||
new Heatmap(chartData, chartSettings, chartTheme).drawHeatmap();
|
|
||||||
} else {
|
|
||||||
const chartTitleElement = document.createElement("div");
|
|
||||||
chartTitleElement.innerHTML = data.chartSettings.chartTitle;
|
|
||||||
chartTitleElement.classList.add("chartTitle");
|
|
||||||
|
|
||||||
const noDataMessageElement = document.createElement("div");
|
|
||||||
noDataMessageElement.classList.add("noDataMessage");
|
|
||||||
const noDataMessageContent = document.createElement("div");
|
|
||||||
noDataMessageContent.innerHTML = data.errorMessage;
|
|
||||||
|
|
||||||
noDataMessageElement.appendChild(noDataMessageContent);
|
|
||||||
|
|
||||||
if (isDarkTheme(chartTheme)) {
|
|
||||||
chartTitleElement.classList.add("dark-theme");
|
|
||||||
noDataMessageElement.classList.add("dark-theme");
|
|
||||||
noDataMessageContent.classList.add("dark-theme");
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement);
|
|
||||||
document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("message", handleMessage, false);
|
|
||||||
sendReadyMessage();
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
type dataPoint = string | number;
|
|
||||||
|
|
||||||
export interface DataPayload {
|
|
||||||
[id: string]: PartitionTimeStampToData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PortalTheme {
|
|
||||||
blue = 1,
|
|
||||||
azure,
|
|
||||||
light,
|
|
||||||
dark,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HeatmapData {
|
|
||||||
yAxisPoints: string[];
|
|
||||||
xAxisPoints: string[];
|
|
||||||
dataPoints: dataPoint[][];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HeatmapCaptions {
|
|
||||||
chartTitle: string;
|
|
||||||
yAxisTitle: string;
|
|
||||||
tooltipText: string;
|
|
||||||
timeWindow: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FontSettings {
|
|
||||||
family: string;
|
|
||||||
size: number;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LayoutSettings {
|
|
||||||
paper_bgcolor?: string;
|
|
||||||
plot_bgcolor?: string;
|
|
||||||
margin?: {
|
|
||||||
l: number;
|
|
||||||
r: number;
|
|
||||||
b: number;
|
|
||||||
t: number;
|
|
||||||
pad: number;
|
|
||||||
};
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
yaxis?: {
|
|
||||||
fixedrange: boolean;
|
|
||||||
title: HeatmapCaptions["yAxisTitle"];
|
|
||||||
titlefont: FontSettings;
|
|
||||||
autorange: boolean;
|
|
||||||
showgrid: boolean;
|
|
||||||
zeroline: boolean;
|
|
||||||
showline: boolean;
|
|
||||||
autotick: boolean;
|
|
||||||
ticks: "";
|
|
||||||
showticklabels: boolean;
|
|
||||||
};
|
|
||||||
xaxis?: {
|
|
||||||
fixedrange: boolean;
|
|
||||||
title: string;
|
|
||||||
titlefont: FontSettings;
|
|
||||||
autorange: boolean;
|
|
||||||
showgrid: boolean;
|
|
||||||
zeroline: boolean;
|
|
||||||
showline: boolean;
|
|
||||||
autotick: boolean;
|
|
||||||
showticklabels: boolean;
|
|
||||||
tickformat: string;
|
|
||||||
tickfont: FontSettings;
|
|
||||||
};
|
|
||||||
title?: {
|
|
||||||
text: HeatmapCaptions["chartTitle"];
|
|
||||||
x: number;
|
|
||||||
font?: FontSettings;
|
|
||||||
};
|
|
||||||
font?: FontSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChartSettings {
|
|
||||||
z: HeatmapData["dataPoints"];
|
|
||||||
type: "heatmap";
|
|
||||||
zmin: number;
|
|
||||||
zmid: number;
|
|
||||||
zmax: number;
|
|
||||||
colorscale: [number, string][];
|
|
||||||
name: string;
|
|
||||||
hovertemplate: HeatmapCaptions["tooltipText"];
|
|
||||||
colorbar: {
|
|
||||||
thickness: number;
|
|
||||||
outlinewidth: number;
|
|
||||||
tickcolor: string;
|
|
||||||
tickfont: FontSettings;
|
|
||||||
};
|
|
||||||
y: HeatmapData["yAxisPoints"];
|
|
||||||
x: HeatmapData["xAxisPoints"];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DisplaySettings {
|
|
||||||
displayModeBar: boolean;
|
|
||||||
responsive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PartitionTimeStampToData {
|
|
||||||
[timeSeriesDates: string]: {
|
|
||||||
[NormalizedThroughput: string]: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
|
import { GlobalSecondaryIndexLabels } from "Common/Constants";
|
||||||
|
import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility";
|
||||||
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
|
import {
|
||||||
|
AddGlobalSecondaryIndexPanel,
|
||||||
|
AddGlobalSecondaryIndexPanelProps,
|
||||||
|
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
||||||
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",
|
||||||
});
|
});
|
||||||
@@ -94,17 +103,23 @@ export const createCollectionContextMenuButton = (
|
|||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled) {
|
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
} else {
|
} else {
|
||||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell",
|
label:
|
||||||
|
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
|
||||||
|
? "Open Mongo Shell"
|
||||||
|
: "New Shell",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useNotebook.getState().isShellEnabled && userContext.apiType === "Cassandra") {
|
if (
|
||||||
|
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
|
||||||
|
userContext.apiType === "Cassandra"
|
||||||
|
) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -143,23 +158,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 (isGlobalSecondaryIndexEnabled() && !selectedCollection.materializedViewDefinition()) {
|
||||||
|
items.push({
|
||||||
|
label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
|
||||||
|
onClick: () => {
|
||||||
|
const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = {
|
||||||
|
explorer: container,
|
||||||
|
sourceContainer: selectedCollection,
|
||||||
|
};
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
|
||||||
|
<AddGlobalSecondaryIndexPanel {...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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -214,8 +214,10 @@ export const Dialog: FC = () => {
|
|||||||
{contentHtml}
|
{contentHtml}
|
||||||
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<PrimaryButton {...primaryButtonProps} />
|
<PrimaryButton {...primaryButtonProps} data-test={`DialogButton:${primaryButtonText}`} />
|
||||||
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
|
{secondaryButtonProps && (
|
||||||
|
<DefaultButton {...secondaryButtonProps} data-test={`DialogButton:${secondaryButtonText}`} />
|
||||||
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</FluentDialog>
|
</FluentDialog>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
315
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
315
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
// 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"
|
||||||
|
data-test={"DocumentsTab/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,3 +1,4 @@
|
|||||||
|
import { AuthType } from "AuthType";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -247,4 +248,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",
|
||||||
|
throughputBucketsEnabled: true,
|
||||||
|
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,11 +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 { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -33,6 +37,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";
|
||||||
@@ -40,6 +45,10 @@ import {
|
|||||||
ConflictResolutionComponent,
|
ConflictResolutionComponent,
|
||||||
ConflictResolutionComponentProps,
|
ConflictResolutionComponentProps,
|
||||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||||
|
import {
|
||||||
|
GlobalSecondaryIndexComponent,
|
||||||
|
GlobalSecondaryIndexComponentProps,
|
||||||
|
} from "./SettingsSubComponents/GlobalSecondaryIndexComponent";
|
||||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
@@ -86,6 +95,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 +115,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 +167,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 isGlobalSecondaryIndex: 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 +185,13 @@ 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.isGlobalSecondaryIndex =
|
||||||
|
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
|
||||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
|
this.isFullTextSearchEnabled = userContext.apiType === "SQL";
|
||||||
|
|
||||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||||
|
this.throughputBucketsEnabled = userContext.throughputBucketsEnabled;
|
||||||
|
|
||||||
// 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 +210,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 +230,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 +343,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 +356,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 +441,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 +460,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 +495,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 +597,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 +756,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 +778,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 +797,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 +872,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 +935,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 +957,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 +999,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 +1009,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 +1066,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,
|
||||||
@@ -984,6 +1091,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
currentOffer: this.collection.offer(),
|
currentOffer: this.collection.offer(),
|
||||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
|
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
|
||||||
|
throughputBuckets: this.throughputBucketsEnabled ? this.state.throughputBuckets : undefined,
|
||||||
};
|
};
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
if (this.state.isAutoPilotSelected) {
|
if (this.state.isAutoPilotSelected) {
|
||||||
@@ -1039,6 +1147,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
database: this.database,
|
database: this.database,
|
||||||
isFixedContainer: this.isFixedContainer,
|
isFixedContainer: this.isFixedContainer,
|
||||||
|
isGlobalSecondaryIndex: this.isGlobalSecondaryIndex,
|
||||||
onThroughputChange: this.onThroughputChange,
|
onThroughputChange: this.onThroughputChange,
|
||||||
throughput: this.state.throughput,
|
throughput: this.state.throughput,
|
||||||
throughputBaseline: this.state.throughputBaseline,
|
throughputBaseline: this.state.throughputBaseline,
|
||||||
@@ -1091,6 +1200,22 @@ 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,
|
||||||
|
isGlobalSecondaryIndex: this.isGlobalSecondaryIndex,
|
||||||
|
};
|
||||||
|
|
||||||
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
||||||
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
||||||
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
||||||
@@ -1142,14 +1267,23 @@ 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(),
|
||||||
|
isReadOnly: isFabricNative(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
|
const globalSecondaryIndexComponentProps: GlobalSecondaryIndexComponentProps = {
|
||||||
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
|
collection: this.collection,
|
||||||
|
explorer: this.props.settingsTab.getContainer(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
@@ -1165,10 +1299,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 +1342,20 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.throughputBucketsEnabled && !hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
|
tabs.push({
|
||||||
|
tab: SettingsV2TabTypes.ThroughputBucketsTab,
|
||||||
|
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isGlobalSecondaryIndex) {
|
||||||
|
tabs.push({
|
||||||
|
tab: SettingsV2TabTypes.GlobalSecondaryIndexTab,
|
||||||
|
content: <GlobalSecondaryIndexComponent {...globalSecondaryIndexComponentProps} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
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,164 @@
|
|||||||
|
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;
|
||||||
|
isGlobalSecondaryIndex?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { collection, container } from "../TestUtils";
|
||||||
|
import { GlobalSecondaryIndexComponent } from "./GlobalSecondaryIndexComponent";
|
||||||
|
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
|
||||||
|
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
|
||||||
|
|
||||||
|
describe("GlobalSecondaryIndexComponent", () => {
|
||||||
|
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(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(true);
|
||||||
|
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).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(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false);
|
||||||
|
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders neither component when both are missing", () => {
|
||||||
|
testCollection.materializedViews(null);
|
||||||
|
testCollection.materializedViewDefinition(null);
|
||||||
|
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
|
||||||
|
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false);
|
||||||
|
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { FontIcon, Link, Stack, Text } from "@fluentui/react";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import React from "react";
|
||||||
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
|
||||||
|
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
|
||||||
|
|
||||||
|
export interface GlobalSecondaryIndexComponentProps {
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexComponentProps> = ({
|
||||||
|
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 }}>
|
||||||
|
{isSourceContainer && (
|
||||||
|
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following indexes 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 global secondary indexes and how to use them.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}
|
||||||
|
{isTargetContainer && <GlobalSecondaryIndexTargetComponent collection={collection} />}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { PrimaryButton } from "@fluentui/react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { collection, container } from "../TestUtils";
|
||||||
|
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
|
||||||
|
|
||||||
|
describe("GlobalSecondaryIndexSourceComponent", () => {
|
||||||
|
let testCollection: typeof collection;
|
||||||
|
let testExplorer: typeof container;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testCollection = { ...collection };
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders without crashing", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
|
||||||
|
);
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the PrimaryButton", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
|
||||||
|
);
|
||||||
|
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates when new global secondary indexes are provided", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<GlobalSecondaryIndexSourceComponent 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,114 @@
|
|||||||
|
import { PrimaryButton } from "@fluentui/react";
|
||||||
|
import { GlobalSecondaryIndexLabels } from "Common/Constants";
|
||||||
|
import { MaterializedView } from "Contracts/DataModels";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { loadMonaco } from "Explorer/LazyMonaco";
|
||||||
|
import { AddGlobalSecondaryIndexPanel } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
||||||
|
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 GlobalSecondaryIndexSourceComponentProps {
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexSourceComponentProps> = ({
|
||||||
|
collection,
|
||||||
|
explorer,
|
||||||
|
}) => {
|
||||||
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
|
||||||
|
|
||||||
|
const globalSecondaryIndexes: MaterializedView[] = collection?.materializedViews() ?? [];
|
||||||
|
|
||||||
|
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedView[] 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 globalSecondaryIndexDefinition = collection.materializedViewDefinition();
|
||||||
|
globalSecondaryIndexDefinition && (definition = globalSecondaryIndexDefinition.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(
|
||||||
|
globalSecondaryIndexes.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: "Global Secondary Index 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 index"
|
||||||
|
styles={{ root: { width: "fit-content", marginTop: 12 } }}
|
||||||
|
onClick={() =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
|
||||||
|
<AddGlobalSecondaryIndexPanel 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 { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
|
||||||
|
|
||||||
|
describe("GlobalSecondaryIndexTargetComponent", () => {
|
||||||
|
let testCollection: Collection;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testCollection = {
|
||||||
|
...collection,
|
||||||
|
materializedViewDefinition: collection.materializedViewDefinition,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders without crashing", () => {
|
||||||
|
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays the source container ID", () => {
|
||||||
|
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
|
||||||
|
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays the global secondary index definition", () => {
|
||||||
|
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
|
||||||
|
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Stack, Text } from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||||
|
|
||||||
|
export interface GlobalSecondaryIndexTargetComponentProps {
|
||||||
|
collection: ViewModels.Collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexTargetComponentProps> = ({
|
||||||
|
collection,
|
||||||
|
}) => {
|
||||||
|
const globalSecondaryIndexDefinition = 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}>Global Secondary Index Settings</Text>
|
||||||
|
|
||||||
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
|
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
|
||||||
|
<Stack styles={valueBoxStyle}>
|
||||||
|
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
|
<Text styles={{ root: { fontWeight: "600" } }}>Global secondary index definition</Text>
|
||||||
|
<Stack styles={valueBoxStyle}>
|
||||||
|
<Text>{globalSecondaryIndexDefinition?.definition}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</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,41 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import {
|
||||||
|
PartitionKeyComponent,
|
||||||
|
PartitionKeyComponentProps,
|
||||||
|
} from "Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
describe("PartitionKeyComponent", () => {
|
||||||
|
// Create a test setup function to get fresh instances for each test
|
||||||
|
const setupTest = () => {
|
||||||
|
// Create an instance of the mocked Explorer
|
||||||
|
const explorer = new Explorer();
|
||||||
|
// Create minimal mock objects for database and collection
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const mockDatabase = {} as any as import("../../../../Contracts/ViewModels").Database;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const mockCollection = {} as any as import("../../../../Contracts/ViewModels").Collection;
|
||||||
|
|
||||||
|
// Create props with the mocked Explorer instance
|
||||||
|
const props: PartitionKeyComponentProps = {
|
||||||
|
database: mockDatabase,
|
||||||
|
collection: mockCollection,
|
||||||
|
explorer,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { explorer, props };
|
||||||
|
};
|
||||||
|
|
||||||
|
it("renders default component and matches snapshot", () => {
|
||||||
|
const { props } = setupTest();
|
||||||
|
const wrapper = shallow(<PartitionKeyComponent {...props} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders read-only component and matches snapshot", () => {
|
||||||
|
const { props } = setupTest();
|
||||||
|
const wrapper = shallow(<PartitionKeyComponent {...props} isReadOnly={true} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 {
|
||||||
@@ -28,16 +29,26 @@ export interface PartitionKeyComponentProps {
|
|||||||
database: ViewModels.Database;
|
database: ViewModels.Database;
|
||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
|
isReadOnly?: boolean; // true: cannot change partition key
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ database, collection, explorer }) => {
|
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||||
|
database,
|
||||||
|
collection,
|
||||||
|
explorer,
|
||||||
|
isReadOnly,
|
||||||
|
}) => {
|
||||||
const { dataTransferJobs } = useDataTransferJobs();
|
const { dataTransferJobs } = useDataTransferJobs();
|
||||||
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
|
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (isReadOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loadDataTransferJobs = refreshDataTransferOperations;
|
const loadDataTransferJobs = refreshDataTransferOperations;
|
||||||
loadDataTransferJobs();
|
loadDataTransferJobs();
|
||||||
}, []);
|
}, [isReadOnly]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const currentJob = findPortalDataTransferJob();
|
const currentJob = findPortalDataTransferJob();
|
||||||
@@ -150,7 +161,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
|||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
|
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
<Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>
|
{!isReadOnly && <Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>}
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
||||||
@@ -162,54 +173,61 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>
|
|
||||||
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the
|
{!isReadOnly && (
|
||||||
source container for the entire duration of the partition key change process.
|
<>
|
||||||
<Link
|
<MessageBar messageBarType={MessageBarType.warning}>
|
||||||
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to
|
||||||
target="_blank"
|
the source container for the entire duration of the partition key change process.
|
||||||
underline
|
<Link
|
||||||
>
|
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
||||||
Learn more
|
target="_blank"
|
||||||
</Link>
|
underline
|
||||||
</MessageBar>
|
>
|
||||||
<Text>
|
Learn more
|
||||||
To change the partition key, a new destination container must be created or an existing destination container
|
</Link>
|
||||||
selected. Data will then be copied to the destination container.
|
</MessageBar>
|
||||||
</Text>
|
<Text>
|
||||||
<PrimaryButton
|
To change the partition key, a new destination container must be created or an existing destination
|
||||||
styles={{ root: { width: "fit-content" } }}
|
container selected. Data will then be copied to the destination container.
|
||||||
text="Change"
|
</Text>
|
||||||
onClick={startPartitionkeyChangeWorkflow}
|
{configContext.platform !== Platform.Emulator && (
|
||||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
<PrimaryButton
|
||||||
/>
|
styles={{ root: { width: "fit-content" } }}
|
||||||
{portalDataTransferJob && (
|
text="Change"
|
||||||
<Stack>
|
onClick={startPartitionkeyChangeWorkflow}
|
||||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||||
<Stack
|
/>
|
||||||
horizontal
|
)}
|
||||||
tokens={{ childrenGap: 20 }}
|
{portalDataTransferJob && (
|
||||||
styles={{
|
<Stack>
|
||||||
root: {
|
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||||
alignItems: "center",
|
<Stack
|
||||||
},
|
horizontal
|
||||||
}}
|
tokens={{ childrenGap: 20 }}
|
||||||
>
|
styles={{
|
||||||
<ProgressIndicator
|
root: {
|
||||||
label={portalDataTransferJob?.properties?.jobName}
|
alignItems: "center",
|
||||||
description={getProgressDescription()}
|
},
|
||||||
percentComplete={getPercentageComplete()}
|
}}
|
||||||
styles={{
|
>
|
||||||
root: {
|
<ProgressIndicator
|
||||||
width: "85%",
|
label={portalDataTransferJob?.properties?.jobName}
|
||||||
},
|
description={getProgressDescription()}
|
||||||
}}
|
percentComplete={getPercentageComplete()}
|
||||||
></ProgressIndicator>
|
styles={{
|
||||||
{isCurrentJobInProgress(portalDataTransferJob) && (
|
root: {
|
||||||
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
|
width: "85%",
|
||||||
)}
|
},
|
||||||
</Stack>
|
}}
|
||||||
</Stack>
|
></ProgressIndicator>
|
||||||
|
{isCurrentJobInProgress(portalDataTransferJob) && (
|
||||||
|
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ describe("ScaleComponent", () => {
|
|||||||
collection: collection,
|
collection: collection,
|
||||||
database: undefined,
|
database: undefined,
|
||||||
isFixedContainer: false,
|
isFixedContainer: false,
|
||||||
|
isGlobalSecondaryIndex: false,
|
||||||
onThroughputChange: () => {
|
onThroughputChange: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface ScaleComponentProps {
|
|||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
database: ViewModels.Database;
|
database: ViewModels.Database;
|
||||||
isFixedContainer: boolean;
|
isFixedContainer: boolean;
|
||||||
|
isGlobalSecondaryIndex: boolean;
|
||||||
onThroughputChange: (newThroughput: number) => void;
|
onThroughputChange: (newThroughput: number) => void;
|
||||||
throughput: number;
|
throughput: number;
|
||||||
throughputBaseline: number;
|
throughputBaseline: number;
|
||||||
@@ -143,6 +144,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
throughputError={this.props.throughputError}
|
throughputError={this.props.throughputError}
|
||||||
instantMaximumThroughput={this.offer?.instantMaximumThroughput}
|
instantMaximumThroughput={this.offer?.instantMaximumThroughput}
|
||||||
softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput}
|
softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput}
|
||||||
|
isGlobalSecondaryIndex={this.props.isGlobalSecondaryIndex}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -143,4 +143,39 @@ describe("SubSettingsComponent", () => {
|
|||||||
expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
|
expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
|
||||||
expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
|
expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uniqueKey is visible", () => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
properties: {
|
||||||
|
capabilities: [{ name: "EnableSQL" }],
|
||||||
|
},
|
||||||
|
} as DatabaseAccount,
|
||||||
|
});
|
||||||
|
const subSettingsComponent = new SubSettingsComponent(baseProps);
|
||||||
|
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uniqueKey not visible due to no keys", () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
...(baseProps.collection.rawDataModel.uniqueKeyPolicy.uniqueKeys = []),
|
||||||
|
};
|
||||||
|
const subSettingsComponent = new SubSettingsComponent(props);
|
||||||
|
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uniqueKey not visible for API", () => {
|
||||||
|
const newContainer = new Explorer();
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
properties: {
|
||||||
|
capabilities: [{ name: "EnableMongo" }],
|
||||||
|
},
|
||||||
|
} as DatabaseAccount,
|
||||||
|
});
|
||||||
|
const props = { ...baseProps, container: newContainer };
|
||||||
|
const subSettingsComponent = new SubSettingsComponent(props);
|
||||||
|
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,12 +63,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
|||||||
private geospatialVisible: boolean;
|
private geospatialVisible: boolean;
|
||||||
private partitionKeyValue: string;
|
private partitionKeyValue: string;
|
||||||
private partitionKeyName: string;
|
private partitionKeyName: string;
|
||||||
|
private uniqueKeyName: string;
|
||||||
|
private uniqueKeyValue: string;
|
||||||
|
|
||||||
constructor(props: SubSettingsComponentProps) {
|
constructor(props: SubSettingsComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.geospatialVisible = userContext.apiType === "SQL";
|
this.geospatialVisible = userContext.apiType === "SQL";
|
||||||
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||||
this.partitionKeyValue = this.getPartitionKeyValue();
|
this.partitionKeyValue = this.getPartitionKeyValue();
|
||||||
|
this.uniqueKeyName = "Unique keys";
|
||||||
|
this.uniqueKeyValue = this.getUniqueKeyValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
@@ -351,6 +355,28 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
|||||||
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
|
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
|
||||||
public isHierarchicalPartitionedContainer = (): boolean => this.props.collection.partitionKey?.kind === "MultiHash";
|
public isHierarchicalPartitionedContainer = (): boolean => this.props.collection.partitionKey?.kind === "MultiHash";
|
||||||
|
|
||||||
|
public getUniqueKeyVisible = (): boolean => {
|
||||||
|
return this.props.collection.rawDataModel.uniqueKeyPolicy?.uniqueKeys.length > 0 && userContext.apiType === "SQL";
|
||||||
|
};
|
||||||
|
|
||||||
|
private getUniqueKeyValue = (): string => {
|
||||||
|
const paths = this.props.collection.rawDataModel.uniqueKeyPolicy?.uniqueKeys?.[0]?.paths;
|
||||||
|
return paths?.join(", ") || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
private getUniqueKeyComponent = (): JSX.Element => (
|
||||||
|
<Stack {...titleAndInputStackProps}>
|
||||||
|
{this.getUniqueKeyVisible() && (
|
||||||
|
<TextField
|
||||||
|
label={this.uniqueKeyName}
|
||||||
|
disabled
|
||||||
|
styles={getTextFieldStyles(undefined, undefined)}
|
||||||
|
defaultValue={this.uniqueKeyValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Stack {...subComponentStackProps}>
|
<Stack {...subComponentStackProps}>
|
||||||
@@ -363,6 +389,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
|||||||
{this.props.changeFeedPolicyVisible && this.getChangeFeedComponent()}
|
{this.props.changeFeedPolicyVisible && this.getChangeFeedComponent()}
|
||||||
|
|
||||||
{this.getPartitionKeyComponent()}
|
{this.getPartitionKeyComponent()}
|
||||||
|
|
||||||
|
{this.getUniqueKeyComponent()}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
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(/Bucket \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(/Bucket \d+/).map((el) => el.textContent);
|
||||||
|
expect(bucketLabels).toEqual([
|
||||||
|
"Bucket 1 (Data Explorer Query Bucket)",
|
||||||
|
"Bucket 2",
|
||||||
|
"Bucket 3",
|
||||||
|
"Bucket 4",
|
||||||
|
"Bucket 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(/Bucket \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(/Bucket \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={`Bucket ${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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -44,6 +44,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
|||||||
},
|
},
|
||||||
instantMaximumThroughput: 5000,
|
instantMaximumThroughput: 5000,
|
||||||
softAllowedMaximumThroughput: 1000000,
|
softAllowedMaximumThroughput: 1000000,
|
||||||
|
isGlobalSecondaryIndex: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("throughput input visible", () => {
|
it("throughput input visible", () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -81,6 +80,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
throughputError?: string;
|
throughputError?: string;
|
||||||
instantMaximumThroughput: number;
|
instantMaximumThroughput: number;
|
||||||
softAllowedMaximumThroughput: number;
|
softAllowedMaximumThroughput: number;
|
||||||
|
isGlobalSecondaryIndex: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThroughputInputAutoPilotV3State {
|
interface ThroughputInputAutoPilotV3State {
|
||||||
@@ -285,7 +285,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
serverId,
|
serverId,
|
||||||
numberOfRegions,
|
numberOfRegions,
|
||||||
isMultimaster,
|
isMultimaster,
|
||||||
true,
|
false,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -366,29 +366,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 (
|
||||||
@@ -399,22 +376,26 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
toolTipElement={getToolTipContainer(this.props.infoBubbleText)}
|
toolTipElement={getToolTipContainer(this.props.infoBubbleText)}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
{this.overrideWithProvisionedThroughputSettings() && (
|
{!this.props.isGlobalSecondaryIndex && (
|
||||||
<MessageBar
|
<>
|
||||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
{this.overrideWithProvisionedThroughputSettings() && (
|
||||||
styles={messageBarStyles}
|
<MessageBar
|
||||||
>
|
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||||
{manualToAutoscaleDisclaimerElement}
|
styles={messageBarStyles}
|
||||||
</MessageBar>
|
>
|
||||||
|
{manualToAutoscaleDisclaimerElement}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
<ChoiceGroup
|
||||||
|
selectedKey={this.props.isAutoPilotSelected.toString()}
|
||||||
|
options={this.options}
|
||||||
|
onChange={this.onChoiceGroupChange}
|
||||||
|
required={this.props.showAsMandatory}
|
||||||
|
ariaLabelledBy={labelId}
|
||||||
|
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected, true)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<ChoiceGroup
|
|
||||||
selectedKey={this.props.isAutoPilotSelected.toString()}
|
|
||||||
options={this.options}
|
|
||||||
onChange={this.onChoiceGroupChange}
|
|
||||||
required={this.props.showAsMandatory}
|
|
||||||
ariaLabelledBy={labelId}
|
|
||||||
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected, true)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -578,26 +559,81 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
private getThroughputTextField = (): JSX.Element => (
|
private getThroughputTextField = (): JSX.Element => (
|
||||||
<>
|
<>
|
||||||
{this.props.isAutoPilotSelected ? (
|
{this.props.isAutoPilotSelected ? (
|
||||||
<TextField
|
<Stack horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
|
||||||
label="Maximum RU/s required by this resource"
|
{/* Column 1: Minimum RU/s */}
|
||||||
required
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
type="number"
|
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||||
id="autopilotInput"
|
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||||
key="auto pilot throughput input"
|
Minimum RU/s
|
||||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
</Text>
|
||||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
|
||||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
</Stack>
|
||||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
<Text
|
||||||
onChange={this.onAutoPilotThroughputChange}
|
style={{
|
||||||
min={autoPilotThroughput1K}
|
fontFamily: "Segoe UI",
|
||||||
onGetErrorMessage={(value: string) => {
|
width: 70,
|
||||||
const sanitizedValue = getSanitizedInputValue(value);
|
height: 28,
|
||||||
return sanitizedValue % 1000
|
border: "none",
|
||||||
? "Throughput value must be in increments of 1000"
|
fontSize: 14,
|
||||||
: this.props.throughputError;
|
backgroundColor: "transparent",
|
||||||
}}
|
fontWeight: 400,
|
||||||
validateOnLoad={false}
|
display: "flex",
|
||||||
/>
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Column 2: "x 10 =" Text */}
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: "Segoe UI",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
paddingBottom: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
x 10 =
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Column 3: Maximum RU/s */}
|
||||||
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
|
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||||
|
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||||
|
Maximum RU/s
|
||||||
|
</Text>
|
||||||
|
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
|
||||||
|
</Stack>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
id="autopilotInput"
|
||||||
|
key="auto pilot throughput input"
|
||||||
|
styles={{
|
||||||
|
...getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline),
|
||||||
|
fieldGroup: { width: 100, height: 28 },
|
||||||
|
field: { fontSize: 14, fontWeight: 400 },
|
||||||
|
}}
|
||||||
|
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||||
|
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||||
|
value={
|
||||||
|
this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()
|
||||||
|
}
|
||||||
|
onChange={this.onAutoPilotThroughputChange}
|
||||||
|
min={autoPilotThroughput1K}
|
||||||
|
onGetErrorMessage={(value: string) => {
|
||||||
|
const sanitizedValue = getSanitizedInputValue(value);
|
||||||
|
return sanitizedValue % 1000
|
||||||
|
? "Throughput value must be in increments of 1000"
|
||||||
|
: this.props.throughputError;
|
||||||
|
}}
|
||||||
|
validateOnLoad={false}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
@@ -661,7 +697,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"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user