mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-29 13:51:49 +00:00
Compare commits
3 Commits
3556812
...
users/lang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c55a7fef7 | ||
|
|
7c6fcb54d0 | ||
|
|
5f2b882eaa |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1 +1 @@
|
||||
[Preview this branch](https://dataexplorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
||||
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
||||
|
||||
103
.github/workflows/ci.yml
vendored
103
.github/workflows/ci.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run build:contracts
|
||||
- name: Restore Build Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: .cache
|
||||
key: ${{ runner.os }}-build-cache
|
||||
@@ -92,20 +92,18 @@ jobs:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
- run: cp -r ./Contracts ./dist/contracts
|
||||
- run: cp -r ./configs ./dist/configs
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: "Az CLI login"
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
|
||||
- name: Upload build to preview blob storage
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --auth-mode login --overwrite true
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
- name: Upload preview config to blob storage
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --auth-mode login --overwrite true
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
@@ -115,21 +113,21 @@ jobs:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/prod.json config.json
|
||||
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text
|
||||
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
|
||||
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
|
||||
- run: dotnet nuget remove source "ADO"
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload package to Artifacts
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v3
|
||||
name: packages
|
||||
with:
|
||||
name: prod-package
|
||||
path: "bin/Release/*.nupkg"
|
||||
|
||||
path: "*.nupkg"
|
||||
nugetmpac:
|
||||
name: Publish Nuget MPAC
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
@@ -139,21 +137,22 @@ jobs:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/mpac.json config.json
|
||||
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
|
||||
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text
|
||||
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
|
||||
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
|
||||
- run: dotnet nuget remove source "ADO"
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload package to Artifacts
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v3
|
||||
name: packages
|
||||
with:
|
||||
name: mpac-package
|
||||
path: "bin/Release/*.nupkg"
|
||||
path: "*.nupkg"
|
||||
|
||||
playwright-tests:
|
||||
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"
|
||||
@@ -186,9 +185,9 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
path: blob-report
|
||||
retention-days: 1
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
path: blob-report
|
||||
retention-days: 1
|
||||
|
||||
merge-playwright-reports:
|
||||
name: "Merge Playwright Reports"
|
||||
@@ -198,26 +197,26 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||
- name: Merge into HTML Report
|
||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
@@ -1,9 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<NoBuild>true</NoBuild>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<NuspecFile>DataExplorer.nuspec</NuspecFile>
|
||||
<NuspecProperties>version=$(PackageVersion)</NuspecProperties>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -61,8 +61,6 @@
|
||||
|
||||
@GalleryBackgroundColor: #fdfdfd;
|
||||
|
||||
@LinkColor: #2d6da4;
|
||||
|
||||
//Icons
|
||||
@InfoIconColor: #0072c6;
|
||||
@WarningIconColor: #db7500;
|
||||
|
||||
@@ -1830,14 +1830,6 @@ input::-webkit-calendar-picker-indicator::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.customAccordion button:focus {
|
||||
.focus();
|
||||
}
|
||||
|
||||
.customAccordion {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.datalist-arrow:after:hover {
|
||||
content: "\276F";
|
||||
position: absolute;
|
||||
|
||||
615
package-lock.json
generated
615
package-lock.json
generated
@@ -12,7 +12,8 @@
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.2.0-beta.1",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "4.5.0",
|
||||
"@azure/identity": "1.5.2",
|
||||
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
@@ -121,7 +122,7 @@
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@playwright/test": "1.44.0",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
@@ -427,68 +428,47 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@azure/identity": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.5.0.tgz",
|
||||
"integrity": "sha512-EknvVmtBuSIic47xkOqyNabAme0RYTw52BTMz8eBgU1ysTyMrD1uOoM+JdS0J/4Yfp98IBT3osqq3BfwSaNaGQ==",
|
||||
"version": "1.5.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-auth": "^1.9.0",
|
||||
"@azure/core-client": "^1.9.2",
|
||||
"@azure/core-rest-pipeline": "^1.17.0",
|
||||
"@azure/core-tracing": "^1.0.0",
|
||||
"@azure/core-util": "^1.11.0",
|
||||
"@azure/core-auth": "^1.3.0",
|
||||
"@azure/core-client": "^1.0.0",
|
||||
"@azure/core-rest-pipeline": "^1.1.0",
|
||||
"@azure/core-tracing": "1.0.0-preview.12",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"@azure/msal-browser": "^3.26.1",
|
||||
"@azure/msal-node": "^2.15.0",
|
||||
"@azure/msal-node": "1.0.0-beta.6",
|
||||
"@types/stoppable": "^1.1.0",
|
||||
"axios": "^0.21.1",
|
||||
"events": "^3.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"open": "^8.0.0",
|
||||
"msal": "^1.0.2",
|
||||
"open": "^7.0.0",
|
||||
"qs": "^6.7.0",
|
||||
"stoppable": "^1.1.0",
|
||||
"tslib": "^2.0.0",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"keytar": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/@azure/core-tracing": {
|
||||
"version": "1.0.0-preview.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/@azure/msal-browser": {
|
||||
"version": "3.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.28.1.tgz",
|
||||
"integrity": "sha512-OHHEWMB5+Zrix8yKvLVzU3rKDFvh7SOzAzXfICD7YgUXLxfHpTPX2pzOotrri1kskwhHqIj4a5LvhZlIqE7C7g==",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "14.16.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/@azure/msal-common": {
|
||||
"version": "14.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz",
|
||||
"integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/open": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
|
||||
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
|
||||
"dependencies": {
|
||||
"define-lazy-prop": "^2.0.0",
|
||||
"is-docker": "^2.1.1",
|
||||
"is-wsl": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
"version": "2.6.2",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@azure/logger": {
|
||||
"version": "1.0.4",
|
||||
@@ -504,6 +484,10 @@
|
||||
"version": "2.6.2",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@azure/ms-rest-azure-env": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@azure/ms-rest-azure-js": {
|
||||
"version": "2.1.0",
|
||||
"license": "MIT",
|
||||
@@ -575,6 +559,15 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/ms-rest-nodeauth": {
|
||||
"version": "3.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/ms-rest-azure-env": "^2.0.0",
|
||||
"@azure/ms-rest-js": "^2.0.4",
|
||||
"adal-node": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-browser": {
|
||||
"version": "2.14.2",
|
||||
"license": "MIT",
|
||||
@@ -596,24 +589,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-node": {
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz",
|
||||
"integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==",
|
||||
"version": "1.0.0-beta.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "14.16.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"@azure/msal-common": "^4.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-node/node_modules/@azure/msal-common": {
|
||||
"version": "14.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz",
|
||||
"integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -10091,6 +10073,13 @@
|
||||
"@octokit/openapi-types": "^19.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.8.0",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@phosphor/algorithm": {
|
||||
"version": "1.2.0",
|
||||
"license": "BSD-3-Clause"
|
||||
@@ -10186,18 +10175,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
|
||||
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
|
||||
"version": "1.44.0",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.49.1"
|
||||
"playwright": "1.44.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test/node_modules/playwright": {
|
||||
"version": "1.44.0",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.44.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
@@ -12736,6 +12741,13 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/stoppable": {
|
||||
"version": "1.1.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/styled-components": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
@@ -13338,6 +13350,61 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adal-node": {
|
||||
"version": "0.2.4",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.3",
|
||||
"async": "^2.6.3",
|
||||
"axios": "^0.21.1",
|
||||
"date-utils": "*",
|
||||
"jws": "3.x.x",
|
||||
"underscore": ">= 1.3.1",
|
||||
"uuid": "^3.1.0",
|
||||
"xpath.js": "~1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/adal-node/node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adal-node/node_modules/async": {
|
||||
"version": "2.6.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/adal-node/node_modules/jwa": {
|
||||
"version": "1.4.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/adal-node/node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/adal-node/node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/address": {
|
||||
"version": "1.1.2",
|
||||
"dev": true,
|
||||
@@ -13970,6 +14037,13 @@
|
||||
"version": "1.12.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-core": {
|
||||
"version": "7.0.0-bridge.0",
|
||||
"dev": true,
|
||||
@@ -14673,7 +14747,7 @@
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -14730,24 +14804,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bintrees": {
|
||||
"version": "1.0.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
@@ -14756,7 +14820,7 @@
|
||||
},
|
||||
"node_modules/bl/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -14772,7 +14836,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
@@ -14780,9 +14843,8 @@
|
||||
},
|
||||
"node_modules/bl/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
@@ -15352,6 +15414,11 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.3",
|
||||
"license": "MIT",
|
||||
@@ -17115,6 +17182,13 @@
|
||||
"version": "1.29.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-utils": {
|
||||
"version": "1.2.21",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.8.19",
|
||||
"license": "MIT"
|
||||
@@ -17209,6 +17283,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"license": "MIT"
|
||||
@@ -17269,6 +17351,7 @@
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -18900,6 +18983,14 @@
|
||||
"version": "2.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
|
||||
@@ -19375,12 +19466,6 @@
|
||||
"version": "2.0.5",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/filesize": {
|
||||
"version": "8.0.7",
|
||||
"dev": true,
|
||||
@@ -19587,7 +19672,6 @@
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.3",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -19895,6 +19979,11 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "7.0.0",
|
||||
"dev": true,
|
||||
@@ -19965,19 +20054,6 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
@@ -20109,6 +20185,11 @@
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"license": "ISC",
|
||||
@@ -21240,7 +21321,7 @@
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -21429,7 +21510,7 @@
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
@@ -24079,24 +24160,6 @@
|
||||
"fsevents": "^1.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-haste-map/node_modules/fsevents": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
|
||||
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
|
||||
"deprecated": "Upgrade to fsevents v2 to mitigate potential security issues",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"nan": "^2.12.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-haste-map/node_modules/jest-worker": {
|
||||
"version": "24.9.0",
|
||||
"license": "MIT",
|
||||
@@ -27465,9 +27528,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"version": "8.5.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
@@ -27478,17 +27540,16 @@
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
"semver": "^5.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
"node": ">=4",
|
||||
"npm": ">=1.4.28"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken/node_modules/jwa": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
@@ -27497,24 +27558,12 @@
|
||||
},
|
||||
"node_modules/jsonwebtoken/node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken/node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jsprim": {
|
||||
"version": "1.4.2",
|
||||
"license": "MIT",
|
||||
@@ -27567,6 +27616,16 @@
|
||||
"version": "2.6.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/keytar": {
|
||||
"version": "7.9.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^4.3.0",
|
||||
"prebuild-install": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"license": "MIT",
|
||||
@@ -27883,8 +27942,7 @@
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.invokemap": {
|
||||
"version": "4.6.0",
|
||||
@@ -27893,8 +27951,7 @@
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
@@ -27902,23 +27959,19 @@
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
@@ -27930,8 +27983,7 @@
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.pullall": {
|
||||
"version": "4.2.0",
|
||||
@@ -29444,6 +29496,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"license": "MIT",
|
||||
@@ -29510,6 +29567,16 @@
|
||||
"version": "2.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msal": {
|
||||
"version": "1.4.18",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multicast-dns": {
|
||||
"version": "7.2.5",
|
||||
"dev": true,
|
||||
@@ -29570,6 +29637,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/native-promise-only": {
|
||||
"version": "0.8.1",
|
||||
"dev": true,
|
||||
@@ -29654,12 +29726,58 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.60.0",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi/node_modules/semver": {
|
||||
"version": "7.6.0",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "4.3.0",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-dir": {
|
||||
"version": "0.1.17",
|
||||
"dev": true,
|
||||
@@ -30680,34 +30798,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
|
||||
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
|
||||
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
|
||||
"version": "1.44.0",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/plotly.js-cartesian-dist-min": {
|
||||
@@ -30858,6 +30957,80 @@
|
||||
"version": "4.2.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.2",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^1.0.1",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"license": "MIT",
|
||||
@@ -31287,6 +31460,28 @@
|
||||
"version": "0.5.1",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/rc/node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/re-resizable": {
|
||||
"version": "6.9.11",
|
||||
"license": "MIT",
|
||||
@@ -34195,6 +34390,45 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.1",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
@@ -36532,6 +36766,13 @@
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/xpath.js": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.2.0-beta.1",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "4.5.0",
|
||||
"@azure/identity": "1.5.2",
|
||||
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
@@ -116,7 +117,7 @@
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@playwright/test": "1.44.0",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
@@ -169,10 +170,10 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-circus": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-html-loader": "1.0.0",
|
||||
"jest-react-hooks-shallow": "1.5.1",
|
||||
"jest-trx-results-processor": "3.0.2",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "11.1.3",
|
||||
"less-vars-loader": "1.1.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[defaults]
|
||||
group = dataexplorer-preview
|
||||
sku = P1V2
|
||||
appserviceplan = dataexplorer-preview
|
||||
location = westus2
|
||||
web = dataexplorer-preview
|
||||
group = stfaul
|
||||
sku = P1v2
|
||||
appserviceplan = stfaul_asp_Linux_centralus_0
|
||||
location = centralus
|
||||
web = cosmos-explorer-preview
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ Cosmos Explorer Preview makes it possible to try a working version of any commit
|
||||
|
||||
Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
|
||||
|
||||
Connection string URLs: https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
|
||||
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
|
||||
Connection string URLs: https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
|
||||
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
|
||||
|
||||
In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"PROXY_PATH": "/proxy",
|
||||
"msalRedirectURI": "https://dataexplorer-preview.azurewebsites.net/"
|
||||
"msalRedirectURI": "https://cosmos-explorer-preview.azurewebsites.net/"
|
||||
}
|
||||
|
||||
@@ -3,15 +3,8 @@ const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
const port = process.env.PORT || 3000;
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
|
||||
const previewStorageWebsiteEndpoint = "https://dataexplorerpreview.z5.web.core.windows.net/";
|
||||
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
|
||||
const githubPullRequestUrl = "https://github.com/Azure/cosmos-explorer/pull";
|
||||
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
|
||||
|
||||
const api = createProxyMiddleware({
|
||||
target: backendEndpoint,
|
||||
const api = createProxyMiddleware("/api", {
|
||||
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
changeOrigin: true,
|
||||
logLevel: "debug",
|
||||
bypass: (req, res) => {
|
||||
@@ -22,8 +15,8 @@ const api = createProxyMiddleware({
|
||||
},
|
||||
});
|
||||
|
||||
const proxy = createProxyMiddleware({
|
||||
target: backendEndpoint,
|
||||
const proxy = createProxyMiddleware("/proxy", {
|
||||
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
@@ -34,38 +27,35 @@ const proxy = createProxyMiddleware({
|
||||
},
|
||||
});
|
||||
|
||||
const commit = createProxyMiddleware({
|
||||
target: previewStorageWebsiteEndpoint,
|
||||
const commit = createProxyMiddleware("/commit", {
|
||||
target: "https://cosmosexplorerpreview.blob.core.windows.net",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
pathRewrite: { "^/commit": "/" },
|
||||
pathRewrite: { "^/commit": "$web/" },
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use("/api", api);
|
||||
app.use("/proxy", proxy);
|
||||
app.use("/commit", commit);
|
||||
app.use(api);
|
||||
app.use(proxy);
|
||||
app.use(commit);
|
||||
app.get("/pull/:pr(\\d+)", (req, res) => {
|
||||
const pr = req.params.pr;
|
||||
if (!/^\d+$/.test(pr)) {
|
||||
return res.status(400).send("Invalid pull request number");
|
||||
}
|
||||
const [, query] = req.originalUrl.split("?");
|
||||
const search = new URLSearchParams(query);
|
||||
|
||||
fetch(`${githubApiUrl}/pulls/${pr}`)
|
||||
fetch("https://api.github.com/repos/Azure/cosmos-explorer/pulls/" + pr)
|
||||
.then((response) => response.json())
|
||||
.then(({ head: { ref, sha } }) => {
|
||||
const prUrl = new URL(`${githubPullRequestUrl}/${pr}`);
|
||||
const prUrl = new URL("https://github.com/Azure/cosmos-explorer/pull/" + pr);
|
||||
prUrl.hash = ref;
|
||||
search.set("feature.pr", prUrl.href);
|
||||
|
||||
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/explorer.html`);
|
||||
const explorer = new URL("https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/explorer.html");
|
||||
explorer.search = search.toString();
|
||||
|
||||
const portal = new URL(azurePortalMpacEndpoint);
|
||||
const portal = new URL("https://ms.portal.azure.com/");
|
||||
portal.searchParams.set("dataExplorerSource", explorer.href);
|
||||
|
||||
return res.redirect(portal.href);
|
||||
@@ -73,10 +63,12 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
|
||||
.catch(() => res.sendStatus(500));
|
||||
});
|
||||
app.get("/", (req, res) => {
|
||||
fetch(`${githubApiUrl}/branches/master`)
|
||||
fetch("https://api.github.com/repos/Azure/cosmos-explorer/branches/master")
|
||||
.then((response) => response.json())
|
||||
.then(({ commit: { sha } }) => {
|
||||
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/hostedExplorer.html`);
|
||||
const explorer = new URL(
|
||||
"https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/hostedExplorer.html"
|
||||
);
|
||||
return res.redirect(explorer.href);
|
||||
})
|
||||
.catch(() => res.sendStatus(500));
|
||||
|
||||
1358
preview/package-lock.json
generated
1358
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2",
|
||||
"deploy": "az webapp up --name \"cosmos-explorer-preview\" --subscription \"cosmosdb-portalteam-generaltest-msft\" --resource-group \"stfaul\"",
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
@@ -12,8 +12,7 @@
|
||||
"author": "Microsoft Corporation",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"node": "^18.20.6",
|
||||
"http-proxy-middleware": "^1.1.0",
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,12 +97,6 @@ export enum CapacityMode {
|
||||
Serverless = "Serverless",
|
||||
}
|
||||
|
||||
export enum WorkloadType {
|
||||
Learning = "Learning",
|
||||
DevelopmentTesting = "Development/Testing",
|
||||
Production = "Production",
|
||||
None = "None",
|
||||
}
|
||||
// flight names returned from the portal are always lowercase
|
||||
export class Flights {
|
||||
public static readonly SettingsV2 = "settingsv2";
|
||||
@@ -125,7 +119,6 @@ export class AfecFeatures {
|
||||
|
||||
export class TagNames {
|
||||
public static defaultExperience: string = "defaultExperience";
|
||||
public static WorkloadType: string = "hidden-workload-type";
|
||||
}
|
||||
|
||||
export class MongoDBAccounts {
|
||||
@@ -156,7 +149,7 @@ export class PortalBackendEndpoints {
|
||||
}
|
||||
|
||||
export class MongoProxyEndpoints {
|
||||
public static readonly Development: string = "https://localhost:7238";
|
||||
public static readonly Local: string = "https://localhost:7238";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
|
||||
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
|
||||
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
||||
@@ -525,11 +518,6 @@ export class PriorityLevel {
|
||||
public static readonly Default = "low";
|
||||
}
|
||||
|
||||
export class ariaLabelForLearnMoreLink {
|
||||
public static readonly AnalyticalStore = "Learn more about analytical store.";
|
||||
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
|
||||
}
|
||||
|
||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizatio
|
||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { PriorityLevel } from "../Common/Constants";
|
||||
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||
@@ -124,6 +125,10 @@ export async function getTokenFromAuthService(
|
||||
resourceType: string,
|
||||
resourceId?: string,
|
||||
): Promise<AuthorizationToken> {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
||||
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
|
||||
}
|
||||
|
||||
try {
|
||||
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
||||
@@ -146,6 +151,34 @@ 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
|
||||
enum SDKSupportedCapabilities {
|
||||
None = 0,
|
||||
@@ -156,24 +189,13 @@ let _client: Cosmos.CosmosClient;
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) {
|
||||
if (!userContext.refreshCosmosClient) {
|
||||
if (!userContext.hasDataPlaneRbacSettingChanged) {
|
||||
return _client;
|
||||
}
|
||||
_client.dispose();
|
||||
_client = null;
|
||||
}
|
||||
|
||||
if (userContext.refreshCosmosClient) {
|
||||
updateUserContext({
|
||||
refreshCosmosClient: false,
|
||||
});
|
||||
}
|
||||
|
||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
_defaultHeaders["x-ms-cosmos-throughput-bucket"] = 1;
|
||||
|
||||
if (
|
||||
userContext.authType === AuthType.ConnectionString ||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { WorkloadType } from "Common/Constants";
|
||||
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||
import { DatabaseAccount, Tags } from "Contracts/DataModels";
|
||||
import { updateUserContext } from "UserContext";
|
||||
|
||||
describe("Database Account Utility", () => {
|
||||
describe("Workload Type", () => {
|
||||
beforeEach(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
tags: {} as Tags,
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
});
|
||||
|
||||
it("Workload Type should return Learning", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
tags: {
|
||||
"hidden-workload-type": WorkloadType.Learning,
|
||||
} as Tags,
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const workloadType: WorkloadType = getWorkloadType();
|
||||
expect(workloadType).toBe(WorkloadType.Learning);
|
||||
});
|
||||
|
||||
it("Workload Type should return None", () => {
|
||||
const workloadType: WorkloadType = getWorkloadType();
|
||||
expect(workloadType).toBe(WorkloadType.None);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TagNames, WorkloadType } from "Common/Constants";
|
||||
import { Tags } from "Contracts/DataModels";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
function isVirtualNetworkFilterEnabled() {
|
||||
@@ -17,12 +15,3 @@ function isPrivateEndpointConnectionsEnabled() {
|
||||
export function isPublicInternetAccessAllowed(): boolean {
|
||||
return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled();
|
||||
}
|
||||
|
||||
export function getWorkloadType(): WorkloadType {
|
||||
const tags: Tags = userContext?.databaseAccount?.tags;
|
||||
const workloadType: WorkloadType = tags && (tags[TagNames.WorkloadType] as WorkloadType);
|
||||
if (!workloadType) {
|
||||
return WorkloadType.None;
|
||||
}
|
||||
return workloadType;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,16 @@ import { configContext, resetConfigContext, updateConfigContext } from "../Confi
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
import {
|
||||
deleteDocument,
|
||||
getEndpoint,
|
||||
getFeatureEndpointOrDefault,
|
||||
queryDocuments,
|
||||
readDocument,
|
||||
updateDocument,
|
||||
} from "./MongoProxyClient";
|
||||
|
||||
const databaseId = "testDB";
|
||||
|
||||
@@ -188,8 +196,20 @@ describe("MongoProxyClient", () => {
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
updateDocument(databaseId, collection, documentId, "{}");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("deleteDocuments", () => {
|
||||
describe("deleteDocument", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateUserContext({
|
||||
@@ -206,9 +226,9 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("builds the correct URL", () => {
|
||||
deleteDocuments(databaseId, collection, [documentId]);
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -218,9 +238,9 @@ describe("MongoProxyClient", () => {
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
deleteDocuments(databaseId, collection, [documentId]);
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -255,4 +275,33 @@ describe("MongoProxyClient", () => {
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFeatureEndpointOrDefault", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
|
||||
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||
});
|
||||
const features = extractFeatures(params);
|
||||
updateUserContext({
|
||||
authType: AuthType.AAD,
|
||||
features: features,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a local endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("DeleteDocument");
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import {
|
||||
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
defaultAllowedMongoProxyEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
import queryString from "querystring";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
@@ -60,6 +67,10 @@ export function queryDocuments(
|
||||
query: string,
|
||||
continuationToken?: string,
|
||||
): Promise<QueryResponse> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) {
|
||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||
}
|
||||
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const params = {
|
||||
@@ -78,7 +89,7 @@ export function queryDocuments(
|
||||
query,
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || "";
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@@ -116,11 +127,76 @@ 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(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) {
|
||||
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
@@ -141,7 +217,7 @@ export function readDocument(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -161,12 +237,61 @@ 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(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
partitionKeyProperty: string,
|
||||
documentContent: unknown,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) {
|
||||
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const params = {
|
||||
@@ -183,7 +308,7 @@ export function createDocument(
|
||||
documentContent: JSON.stringify(documentContent),
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createDocument`, {
|
||||
@@ -203,12 +328,54 @@ 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(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
documentContent: string,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) {
|
||||
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
@@ -229,7 +396,7 @@ export function updateDocument(
|
||||
: "",
|
||||
documentContent,
|
||||
};
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -250,6 +417,139 @@ 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(MongoProxyApi.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(MongoProxyApi.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(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
@@ -275,7 +575,7 @@ export function deleteDocuments(
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
};
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/bulkdelete`, {
|
||||
@@ -299,6 +599,9 @@ export function deleteDocuments(
|
||||
export function createMongoCollectionWithProxy(
|
||||
params: DataModels.CreateCollectionParams,
|
||||
): Promise<DataModels.Collection> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) {
|
||||
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const shardKey: string = params.partitionKey?.paths[0];
|
||||
|
||||
@@ -319,7 +622,7 @@ export function createMongoCollectionWithProxy(
|
||||
isSharded: !!shardKey,
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createCollection`, {
|
||||
@@ -339,6 +642,70 @@ 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 {
|
||||
const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [
|
||||
...defaultAllowedMongoProxyEndpoints,
|
||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
];
|
||||
endpoint =
|
||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||
? userContext.features.mongoProxyEndpoint
|
||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
return getEndpoint(endpoint);
|
||||
}
|
||||
|
||||
export function getEndpoint(endpoint: string): string {
|
||||
let url = endpoint + "/api/mongo/explorer";
|
||||
|
||||
@@ -352,6 +719,84 @@ export function getEndpoint(endpoint: string): string {
|
||||
return url;
|
||||
}
|
||||
|
||||
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
|
||||
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
|
||||
[MongoProxyApi.ResourceList]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.QueryDocuments]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.CreateDocument]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.ReadDocument]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.UpdateDocument]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.DeleteDocument]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.CreateCollectionWithProxy]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.LegacyMongoShell]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.BulkDelete]: [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
};
|
||||
|
||||
if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (configContext.globallyEnabledMongoAPIs.includes(mongoProxyApi)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT);
|
||||
}
|
||||
|
||||
export class ThrottlingError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
||||
@@ -105,8 +105,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
? parseInt(resource.softAllowedMaximumThroughput)
|
||||
: resource.softAllowedMaximumThroughput;
|
||||
|
||||
const throughputBuckets = resource?.throughputBuckets;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
id: offerId,
|
||||
@@ -116,7 +114,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
throughputBuckets,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,7 +125,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
throughputBuckets,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import {
|
||||
migrateCassandraKeyspaceToAutoscale,
|
||||
@@ -359,13 +359,6 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
defaultAllowedArmEndpoints,
|
||||
defaultAllowedBackendEndpoints,
|
||||
@@ -49,8 +50,10 @@ export interface ConfigContext {
|
||||
CATALOG_API_KEY: string;
|
||||
ARCADIA_ENDPOINT: string;
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||
BACKEND_ENDPOINT?: string;
|
||||
PORTAL_BACKEND_ENDPOINT: string;
|
||||
NEW_BACKEND_APIS?: BackendApi[];
|
||||
MONGO_BACKEND_ENDPOINT?: string;
|
||||
MONGO_PROXY_ENDPOINT: string;
|
||||
CASSANDRA_PROXY_ENDPOINT: string;
|
||||
NEW_CASSANDRA_APIS?: string[];
|
||||
@@ -88,7 +91,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
||||
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net$`,
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
@@ -106,6 +109,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
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
|
||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||
@@ -148,6 +152,15 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.ARCADIA_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.BACKEND_ENDPOINT,
|
||||
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
||||
)
|
||||
) {
|
||||
delete newContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.MONGO_PROXY_ENDPOINT,
|
||||
@@ -157,6 +170,10 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
||||
|
||||
@@ -9,7 +9,6 @@ export enum TabKind {
|
||||
Graph,
|
||||
SQLQuery,
|
||||
ScaleSettings,
|
||||
MongoQuery,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,8 +51,6 @@ export interface OpenCollectionTab extends OpenTab {
|
||||
*/
|
||||
export interface OpenQueryTab extends OpenCollectionTab {
|
||||
query: QueryInfo;
|
||||
splitterDirection?: "vertical" | "horizontal";
|
||||
queryViewSizePercent?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,6 @@ export interface ArmEntity {
|
||||
location: string;
|
||||
type: string;
|
||||
kind: string;
|
||||
tags?: Tags;
|
||||
}
|
||||
|
||||
export interface DatabaseAccount extends ArmEntity {
|
||||
@@ -275,12 +274,6 @@ export interface Offer {
|
||||
offerReplacePending: boolean;
|
||||
instantMaximumThroughput?: number;
|
||||
softAllowedMaximumThroughput?: number;
|
||||
throughputBuckets?: ThroughputBucket[];
|
||||
}
|
||||
|
||||
export interface ThroughputBucket {
|
||||
id: number;
|
||||
maxThroughputPercentage: number;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
@@ -403,7 +396,6 @@ export interface UpdateOfferParams {
|
||||
collectionId?: string;
|
||||
migrateToAutoPilot?: boolean;
|
||||
migrateToManual?: boolean;
|
||||
throughputBuckets?: ThroughputBucket[];
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
@@ -671,5 +663,3 @@ export interface FeatureRegistration {
|
||||
state: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Tags = { [key: string]: string };
|
||||
|
||||
@@ -41,7 +41,7 @@ export enum MessageTypes {
|
||||
OpenPostgreSQLPasswordReset,
|
||||
OpenPostgresNetworkingBlade,
|
||||
OpenCosmosDBNetworkingBlade,
|
||||
DisplayNPSSurvey, // unused
|
||||
DisplayNPSSurvey,
|
||||
OpenVCoreMongoNetworkingBlade,
|
||||
OpenVCoreMongoConnectionStringsBlade,
|
||||
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
|
||||
|
||||
@@ -115,13 +115,7 @@ export interface CollectionBase extends TreeNode {
|
||||
isSampleCollection?: boolean;
|
||||
|
||||
onDocumentDBDocumentsClick(): void;
|
||||
onNewQueryClick(
|
||||
source: any,
|
||||
event?: MouseEvent,
|
||||
queryText?: string,
|
||||
splitterDirection?: "horizontal" | "vertical",
|
||||
queryViewSizePercent?: number,
|
||||
): void;
|
||||
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
||||
expandCollection(): void;
|
||||
collapseCollection(): void;
|
||||
getDatabase(): Database;
|
||||
@@ -157,13 +151,7 @@ export interface Collection extends CollectionBase {
|
||||
onSettingsClick: () => Promise<void>;
|
||||
|
||||
onNewGraphClick(): void;
|
||||
onNewMongoQueryClick(
|
||||
source: any,
|
||||
event?: MouseEvent,
|
||||
queryText?: string,
|
||||
splitterDirection?: "horizontal" | "vertical",
|
||||
queryViewSizePercent?: number,
|
||||
): void;
|
||||
onNewMongoQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
||||
onNewMongoShellClick(): void;
|
||||
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
||||
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
||||
@@ -323,8 +311,6 @@ export interface QueryTabOptions extends TabOptions {
|
||||
partitionKey?: DataModels.PartitionKey;
|
||||
queryText?: string;
|
||||
resourceTokenPartitionKey?: string;
|
||||
splitterDirection?: "horizontal" | "vertical";
|
||||
queryViewSizePercent?: number;
|
||||
}
|
||||
|
||||
export interface ScriptTabOption extends TabOptions {
|
||||
@@ -406,6 +392,7 @@ export interface DataExplorerInputsFrame {
|
||||
csmEndpoint?: string;
|
||||
dnsSuffix?: string;
|
||||
serverId?: string;
|
||||
extensionEndpoint?: string;
|
||||
portalBackendEndpoint?: string;
|
||||
mongoProxyEndpoint?: string;
|
||||
cassandraProxyEndpoint?: string;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { AuthType } from "AuthType";
|
||||
import { shallow } from "enzyme";
|
||||
import ko from "knockout";
|
||||
import { Features } from "Platform/Hosted/extractFeatures";
|
||||
import React from "react";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
@@ -249,42 +247,4 @@ describe("SettingsComponent", () => {
|
||||
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
||||
});
|
||||
|
||||
it("should save throughput bucket changes when Save button is clicked", async () => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
features: { enableThroughputBuckets: true } as Features,
|
||||
authType: AuthType.AAD,
|
||||
});
|
||||
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
const isEnabled = settingsComponentInstance["throughputBucketsEnabled"];
|
||||
expect(isEnabled).toBe(true);
|
||||
|
||||
wrapper.setState({
|
||||
isThroughputBucketsSaveable: true,
|
||||
throughputBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
});
|
||||
|
||||
await settingsComponentInstance.onSaveClick();
|
||||
|
||||
expect(updateOffer).toHaveBeenCalledWith({
|
||||
databaseId: collection.databaseId,
|
||||
collectionId: collection.id(),
|
||||
currentOffer: expect.any(Object),
|
||||
autopilotThroughput: collection.offer().autoscaleMaxThroughput,
|
||||
manualThroughput: collection.offer().manualThroughput,
|
||||
throughputBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
});
|
||||
|
||||
expect(wrapper.state("isThroughputBucketsSaveable")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,10 +7,6 @@ import {
|
||||
ContainerPolicyComponent,
|
||||
ContainerPolicyComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
|
||||
import {
|
||||
ThroughputBucketsComponent,
|
||||
ThroughputBucketsComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
@@ -90,8 +86,6 @@ export interface SettingsComponentState {
|
||||
wasAutopilotOriginallySet: boolean;
|
||||
isScaleSaveable: boolean;
|
||||
isScaleDiscardable: boolean;
|
||||
throughputBuckets: DataModels.ThroughputBucket[];
|
||||
throughputBucketsBaseline: DataModels.ThroughputBucket[];
|
||||
throughputError: string;
|
||||
|
||||
timeToLive: TtlType;
|
||||
@@ -110,7 +104,6 @@ export interface SettingsComponentState {
|
||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||
isSubSettingsSaveable: boolean;
|
||||
isSubSettingsDiscardable: boolean;
|
||||
isThroughputBucketsSaveable: boolean;
|
||||
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
|
||||
@@ -165,7 +158,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private isVectorSearchEnabled: boolean;
|
||||
private isFullTextSearchEnabled: boolean;
|
||||
private totalThroughputUsed: number;
|
||||
private throughputBucketsEnabled: boolean;
|
||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||
|
||||
constructor(props: SettingsComponentProps) {
|
||||
@@ -183,10 +175,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||
|
||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||
this.throughputBucketsEnabled =
|
||||
userContext.apiType === "SQL" &&
|
||||
userContext.features.enableThroughputBuckets &&
|
||||
userContext.authType === AuthType.AAD;
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
@@ -205,8 +193,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
wasAutopilotOriginallySet: false,
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
throughputBuckets: undefined,
|
||||
throughputBucketsBaseline: undefined,
|
||||
throughputError: undefined,
|
||||
|
||||
timeToLive: undefined,
|
||||
@@ -225,7 +211,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
changeFeedPolicyBaseline: undefined,
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isThroughputBucketsSaveable: false,
|
||||
|
||||
vectorEmbeddingPolicy: undefined,
|
||||
vectorEmbeddingPolicyBaseline: undefined,
|
||||
@@ -342,8 +327,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) ||
|
||||
this.state.isThroughputBucketsSaveable
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -355,8 +339,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) ||
|
||||
this.state.isThroughputBucketsSaveable
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -436,8 +419,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
this.setState({
|
||||
throughput: this.state.throughputBaseline,
|
||||
throughputBuckets: this.state.throughputBucketsBaseline,
|
||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
||||
timeToLive: this.state.timeToLiveBaseline,
|
||||
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
||||
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
||||
@@ -460,7 +441,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
isSubSettingsSaveable: false,
|
||||
isThroughputBucketsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isContainerPolicyDirty: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
@@ -499,10 +479,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
||||
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 });
|
||||
@@ -773,13 +749,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
] as DataModels.ComputedProperties;
|
||||
}
|
||||
|
||||
const throughputBuckets = this.offer?.throughputBuckets;
|
||||
|
||||
return {
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
throughputBuckets,
|
||||
throughputBucketsBaseline: throughputBuckets,
|
||||
changeFeedPolicy: changeFeedPolicy,
|
||||
changeFeedPolicyBaseline: changeFeedPolicy,
|
||||
timeToLive: timeToLive,
|
||||
@@ -867,10 +839,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.setState({ throughput: newThroughput, throughputError });
|
||||
};
|
||||
|
||||
private onThroughputBucketChange = (throughputBuckets: DataModels.ThroughputBucket[]): void => {
|
||||
this.setState({ throughputBuckets });
|
||||
};
|
||||
|
||||
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||
|
||||
@@ -1061,24 +1029,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
}
|
||||
|
||||
if (this.throughputBucketsEnabled && this.state.isThroughputBucketsSaveable) {
|
||||
const updatedOffer: DataModels.Offer = await updateOffer({
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
|
||||
? this.collection.offer().autoscaleMaxThroughput
|
||||
: undefined,
|
||||
manualThroughput: this.collection.offer().manualThroughput
|
||||
? this.collection.offer().manualThroughput
|
||||
: undefined,
|
||||
throughputBuckets: this.state.throughputBuckets,
|
||||
});
|
||||
this.collection.offer(updatedOffer);
|
||||
this.offer = updatedOffer;
|
||||
this.setState({ isThroughputBucketsSaveable: false });
|
||||
}
|
||||
|
||||
if (this.state.isScaleSaveable) {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
@@ -1259,13 +1209,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||
};
|
||||
|
||||
const throughputBucketsComponentProps: ThroughputBucketsComponentProps = {
|
||||
currentBuckets: this.state.throughputBuckets,
|
||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
||||
onBucketsChange: this.onThroughputBucketChange,
|
||||
onSaveableChange: this.onThroughputBucketsSaveableChange,
|
||||
};
|
||||
|
||||
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
||||
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
||||
collection: this.collection,
|
||||
@@ -1328,13 +1271,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
});
|
||||
}
|
||||
|
||||
if (this.throughputBucketsEnabled) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ThroughputBucketsTab,
|
||||
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||
|
||||
@@ -14,7 +14,6 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
||||
import {
|
||||
@@ -178,14 +177,12 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
||||
To change the partition key, a new destination container must be created or an existing destination container
|
||||
selected. Data will then be copied to the destination container.
|
||||
</Text>
|
||||
{configContext.platform !== Platform.Emulator && (
|
||||
<PrimaryButton
|
||||
styles={{ root: { width: "fit-content" } }}
|
||||
text="Change"
|
||||
onClick={startPartitionkeyChangeWorkflow}
|
||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||
/>
|
||||
)}
|
||||
<PrimaryButton
|
||||
styles={{ root: { width: "fit-content" } }}
|
||||
text="Change"
|
||||
onClick={startPartitionkeyChangeWorkflow}
|
||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||
/>
|
||||
{portalDataTransferJob && (
|
||||
<Stack>
|
||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { ThroughputBucketsComponent } from "./ThroughputBucketsComponent";
|
||||
|
||||
describe("ThroughputBucketsComponent", () => {
|
||||
const mockOnBucketsChange = jest.fn();
|
||||
const mockOnSaveableChange = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
currentBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
throughputBucketsBaseline: [
|
||||
{ id: 1, maxThroughputPercentage: 40 },
|
||||
{ id: 2, maxThroughputPercentage: 50 },
|
||||
],
|
||||
onBucketsChange: mockOnBucketsChange,
|
||||
onSaveableChange: mockOnSaveableChange,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders the correct number of buckets", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("renders buckets in the correct order even if input is unordered", () => {
|
||||
const unorderedBuckets = [
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
];
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
|
||||
|
||||
const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent);
|
||||
expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]);
|
||||
});
|
||||
|
||||
it("renders all provided buckets even if they exceed the max default bucket count", () => {
|
||||
const oversizedBuckets = [
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 70 },
|
||||
{ id: 4, maxThroughputPercentage: 80 },
|
||||
{ id: 5, maxThroughputPercentage: 90 },
|
||||
{ id: 6, maxThroughputPercentage: 100 },
|
||||
{ id: 7, maxThroughputPercentage: 40 },
|
||||
];
|
||||
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
|
||||
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7);
|
||||
|
||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("60")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("70")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("80")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("90")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("100")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onBucketsChange when a bucket value changes", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "70" } });
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("triggers onSaveableChange when values change", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "80" } });
|
||||
|
||||
expect(mockOnSaveableChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("updates state consistently after multiple changes to different buckets", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
|
||||
const input1 = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input1, { target: { value: "70" } });
|
||||
|
||||
const input2 = screen.getByDisplayValue("60");
|
||||
fireEvent.change(input2, { target: { value: "80" } });
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 80 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("resets to baseline when currentBuckets are reset", () => {
|
||||
const { rerender } = render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input1 = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input1, { target: { value: "70" } });
|
||||
|
||||
rerender(<ThroughputBucketsComponent {...defaultProps} currentBuckets={defaultProps.throughputBucketsBaseline} />);
|
||||
|
||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not call onBucketsChange when value remains unchanged", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "50" } });
|
||||
|
||||
expect(mockOnBucketsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disables input and slider when maxThroughputPercentage is 100", () => {
|
||||
render(
|
||||
<ThroughputBucketsComponent
|
||||
{...defaultProps}
|
||||
currentBuckets={[
|
||||
{ id: 1, maxThroughputPercentage: 100 },
|
||||
{ id: 2, maxThroughputPercentage: 50 },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const disabledInputs = screen.getAllByDisplayValue("100");
|
||||
expect(disabledInputs.length).toBeGreaterThan(0);
|
||||
expect(disabledInputs[0]).toBeDisabled();
|
||||
|
||||
const sliders = screen.getAllByRole("slider");
|
||||
expect(sliders.length).toBeGreaterThan(0);
|
||||
expect(sliders[0]).toHaveAttribute("aria-disabled", "true");
|
||||
expect(sliders[1]).toHaveAttribute("aria-disabled", "false");
|
||||
});
|
||||
|
||||
it("toggles bucket value between 50 and 100 with switch", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const toggles = screen.getAllByRole("switch");
|
||||
|
||||
fireEvent.click(toggles[0]);
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 100 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
|
||||
fireEvent.click(toggles[0]);
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("ensures default buckets are used when no buckets are provided", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
||||
import { ThroughputBucket } from "Contracts/DataModels";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { isDirty } from "../../SettingsUtils";
|
||||
|
||||
const MAX_BUCKET_SIZES = 5;
|
||||
|
||||
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
}));
|
||||
|
||||
export interface ThroughputBucketsComponentProps {
|
||||
currentBuckets: ThroughputBucket[];
|
||||
throughputBucketsBaseline: ThroughputBucket[];
|
||||
onBucketsChange: (updatedBuckets: ThroughputBucket[]) => void;
|
||||
onSaveableChange: (isSaveable: boolean) => void;
|
||||
}
|
||||
|
||||
export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = ({
|
||||
currentBuckets,
|
||||
throughputBucketsBaseline,
|
||||
onBucketsChange,
|
||||
onSaveableChange,
|
||||
}) => {
|
||||
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
||||
if (!buckets || buckets.length === 0) {
|
||||
return DEFAULT_BUCKETS;
|
||||
}
|
||||
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
||||
const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
}));
|
||||
|
||||
return adjustedDefaultBuckets.map(
|
||||
(defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
|
||||
);
|
||||
};
|
||||
|
||||
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
||||
|
||||
useEffect(() => {
|
||||
setThroughputBuckets(getThroughputBuckets(currentBuckets));
|
||||
onSaveableChange(false);
|
||||
}, [currentBuckets]);
|
||||
|
||||
useEffect(() => {
|
||||
const isChanged = isDirty(throughputBuckets, getThroughputBuckets(throughputBucketsBaseline));
|
||||
onSaveableChange(isChanged);
|
||||
}, [throughputBuckets]);
|
||||
|
||||
const handleBucketChange = (id: number, newValue: number) => {
|
||||
const updatedBuckets = throughputBuckets.map((bucket) =>
|
||||
bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
|
||||
);
|
||||
setThroughputBuckets(updatedBuckets);
|
||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
||||
settingsChanged && onBucketsChange(updatedBuckets);
|
||||
};
|
||||
|
||||
const onToggle = (id: number, checked: boolean) => {
|
||||
handleBucketChange(id, checked ? 50 : 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
||||
<Label>Throughput Buckets</Label>
|
||||
<Stack>
|
||||
{throughputBuckets?.map((bucket) => (
|
||||
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
|
||||
<Slider
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
value={bucket.maxThroughputPercentage}
|
||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
||||
showValue={false}
|
||||
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
||||
styles={{ root: { flex: 2, maxWidth: 400 } }}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
<TextField
|
||||
value={bucket.maxThroughputPercentage.toString()}
|
||||
onChange={(event, newValue) => handleBucketChange(bucket.id, parseInt(newValue || "0", 10))}
|
||||
type="number"
|
||||
suffix="%"
|
||||
styles={{
|
||||
fieldGroup: { width: 80 },
|
||||
}}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
<Toggle
|
||||
onText="Active"
|
||||
offText="Inactive"
|
||||
checked={bucket.maxThroughputPercentage !== 100}
|
||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
||||
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
||||
></Toggle>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -17,13 +17,14 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import React from "react";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
||||
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
||||
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
PriceBreakdown,
|
||||
@@ -365,6 +366,29 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
});
|
||||
};
|
||||
|
||||
private minRUperGBSurvey = (): JSX.Element => {
|
||||
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
|
||||
const oneTBinKB = 1000000000;
|
||||
const minRUperGB = 10;
|
||||
const featureFlagEnabled = userContext.features.showMinRUSurvey;
|
||||
const collectionIsEligible =
|
||||
userContext.subscriptionType !== SubscriptionType.Internal &&
|
||||
this.props.usageSizeInKB > oneTBinKB &&
|
||||
this.props.minimum >= usageInGB(this.props.usageSizeInKB) * minRUperGB;
|
||||
if (featureFlagEnabled || collectionIsEligible) {
|
||||
return (
|
||||
<Text>
|
||||
Need to scale below {this.props.minimum} RU/s? Reach out by filling{" "}
|
||||
<a target="_blank" rel="noreferrer" href={href}>
|
||||
this questionnaire
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private renderThroughputModeChoices = (): JSX.Element => {
|
||||
const labelId = "settingsV2RadioButtonLabelId";
|
||||
return (
|
||||
@@ -637,6 +661,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
|
||||
@@ -11,8 +11,7 @@ export type isDirtyTypes =
|
||||
| DataModels.IndexingPolicy
|
||||
| DataModels.ComputedProperties
|
||||
| DataModels.VectorEmbedding[]
|
||||
| DataModels.FullTextPolicy
|
||||
| DataModels.ThroughputBucket[];
|
||||
| DataModels.FullTextPolicy;
|
||||
export const TtlOff = "off";
|
||||
export const TtlOn = "on";
|
||||
export const TtlOnNoDefault = "on-nodefault";
|
||||
@@ -56,7 +55,6 @@ export enum SettingsV2TabTypes {
|
||||
PartitionKeyTab,
|
||||
ComputedPropertiesTab,
|
||||
ContainerVectorPolicyTab,
|
||||
ThroughputBucketsTab,
|
||||
}
|
||||
|
||||
export enum ContainerPolicyTabTypes {
|
||||
@@ -169,8 +167,6 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
return "Computed Properties";
|
||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||
return "Container Policies";
|
||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
||||
return "Throughput Buckets";
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
||||
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
@@ -35,15 +34,10 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
setIsThroughputCapExceeded,
|
||||
onCostAcknowledgeChange,
|
||||
}: ThroughputInputProps) => {
|
||||
const defaultThroughput: number =
|
||||
isFreeTier ||
|
||||
isQuickstart ||
|
||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType())
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
|
||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
||||
const [throughput, setThroughput] = useState<number>(
|
||||
isFreeTier || isQuickstart ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K,
|
||||
);
|
||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||
const [throughputError, setThroughputError] = useState<string>("");
|
||||
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||
@@ -53,6 +47,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||
|
||||
useEffect(() => {
|
||||
// throughput cap check for the initial state
|
||||
let totalThroughput = 0;
|
||||
@@ -162,6 +157,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||
if (mode === "Autoscale") {
|
||||
const defaultThroughput = isFreeTier
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
setThroughput(defaultThroughput);
|
||||
setIsAutoScaleSelected(true);
|
||||
setThroughputValue(defaultThroughput);
|
||||
|
||||
@@ -35,7 +35,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { isAccountNewerThanThresholdInMs, updateUserContext, userContext } from "../UserContext";
|
||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
@@ -278,6 +278,37 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public openNPSSurveyDialog(): void {
|
||||
if (!Platform.Portal || !["Postgres", "SQL", "Mongo"].includes(userContext.apiType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ONE_DAY_IN_MS = 86400000;
|
||||
const SEVEN_DAYS_IN_MS = 604800000;
|
||||
|
||||
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
|
||||
if (userContext.isTryCosmosDBSubscription) {
|
||||
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
|
||||
Logger.logInfo(
|
||||
`Sending message to Portal to check if NPS Survey can be displayed in Try Cosmos DB ${userContext.apiType}`,
|
||||
"Explorer/openNPSSurveyDialog",
|
||||
);
|
||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||
}
|
||||
} else {
|
||||
// Show survey when an existing account is older than 7 days
|
||||
if (
|
||||
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", SEVEN_DAYS_IN_MS)
|
||||
) {
|
||||
Logger.logInfo(
|
||||
`Sending message to Portal to check if NPS Survey can be displayed for existing ${userContext.apiType} account older than 7 days`,
|
||||
"Explorer/openNPSSurveyDialog",
|
||||
);
|
||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async openCESCVAFeedbackBlade(): Promise<void> {
|
||||
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
||||
Logger.logInfo(
|
||||
@@ -1103,7 +1134,7 @@ export default class Explorer {
|
||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
||||
: this.refreshAllDatabases();
|
||||
}
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
@@ -1127,7 +1158,7 @@ export default class Explorer {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
}
|
||||
|
||||
this.refreshSampleData();
|
||||
await this.refreshSampleData();
|
||||
}
|
||||
|
||||
public async configureCopilot(): Promise<void> {
|
||||
@@ -1152,27 +1183,26 @@ export default class Explorer {
|
||||
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
||||
}
|
||||
|
||||
public refreshSampleData(): void {
|
||||
if (!userContext.sampleDataConnectionInfo) {
|
||||
public async refreshSampleData(): Promise<void> {
|
||||
try {
|
||||
if (!userContext.sampleDataConnectionInfo) {
|
||||
return;
|
||||
}
|
||||
const collection: DataModels.Collection = await readSampleCollection();
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||
if (!databaseId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "Explorer");
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||
if (!databaseId) {
|
||||
return;
|
||||
}
|
||||
|
||||
readSampleCollection()
|
||||
.then((collection: DataModels.Collection) => {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
// @ConsoleHeaderHeight: 32px;
|
||||
@ConsoleHeaderHeight: 32px;
|
||||
@ConsoleContentsPaneHeight: 220px;
|
||||
@ConsoleStatusMaxWidth: 672px;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
// height: @ConsoleHeaderHeight;
|
||||
height: @ConsoleHeaderHeight;
|
||||
width: 100%;
|
||||
background-color: @NotificationLow;
|
||||
border-top: @ButtonBorderWidth @BaseMedium solid;
|
||||
@@ -38,9 +38,6 @@
|
||||
}
|
||||
|
||||
.statusBar {
|
||||
width: 100%;
|
||||
padding: 6px 0px;
|
||||
|
||||
.dataTypeIcons {
|
||||
cursor: pointer;
|
||||
margin: 0px @DefaultSpace 0px @MediumSpace;
|
||||
@@ -79,14 +76,11 @@
|
||||
max-width: @ConsoleStatusMaxWidth;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.expandCollapseButton {
|
||||
cursor: pointer;
|
||||
padding: 6px 5px 6px 0px;
|
||||
padding-right: 5px;
|
||||
|
||||
img {
|
||||
width: @ExpandCollapseIconSize;
|
||||
|
||||
@@ -81,9 +81,9 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
// public setElememntRef = (element: HTMLElement): void => {
|
||||
// this.consoleHeaderElement = element;
|
||||
// };
|
||||
public setElememntRef = (element: HTMLElement): void => {
|
||||
this.consoleHeaderElement = element;
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const numInProgress = this.state.allConsoleData.filter(
|
||||
@@ -98,25 +98,26 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
|
||||
return (
|
||||
<div className="notificationConsoleContainer">
|
||||
<div className="notificationConsoleHeader" id="notificationConsoleHeader">
|
||||
<div
|
||||
className="statusBar"
|
||||
// ref={this.setElememntRef}
|
||||
onClick={() => this.expandCollapseConsole()}
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
ref={this.setElememntRef}
|
||||
onClick={() => this.expandCollapseConsole()}
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="statusBar">
|
||||
<span className="dataTypeIcons">
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={LoadingIcon} alt="In progress items" />
|
||||
<img src={LoadingIcon} alt="in progress items" />
|
||||
<span className="numInProgress">{numInProgress}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={ErrorBlackIcon} alt="Error items" />
|
||||
<img src={ErrorBlackIcon} alt="error items" />
|
||||
<span className="numErroredItems">{numErroredItems}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={infoBubbleIcon} alt="Info items" />
|
||||
<img src={infoBubbleIcon} alt="info items" />
|
||||
<span className="numInfoItems">{numInfoItems}</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -133,14 +134,12 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Console"
|
||||
aria-expanded={this.props.isConsoleExpanded}
|
||||
onClick={() => this.expandCollapseConsole()}
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||
aria-expanded={!this.props.isConsoleExpanded}
|
||||
>
|
||||
<img
|
||||
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
|
||||
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -260,9 +259,9 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
|
||||
private onConsoleWasExpanded = (): void => {
|
||||
// if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
|
||||
// this.consoleHeaderElement.focus();
|
||||
// }
|
||||
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
|
||||
this.consoleHeaderElement.focus();
|
||||
}
|
||||
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="In progress items"
|
||||
alt="in progress items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -34,7 +34,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="Error items"
|
||||
alt="error items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -47,7 +47,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="Info items"
|
||||
alt="info items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -71,15 +71,15 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={false}
|
||||
aria-label="Console"
|
||||
aria-expanded={true}
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt="Expand icon"
|
||||
alt="ChevronUpIcon"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
@@ -192,7 +192,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="In progress items"
|
||||
alt="in progress items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -205,7 +205,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="Error items"
|
||||
alt="error items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -218,7 +218,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="Info items"
|
||||
alt="info items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -244,15 +244,15 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={false}
|
||||
aria-label="Console"
|
||||
aria-expanded={true}
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt="Expand icon"
|
||||
alt="ChevronUpIcon"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { cloneDeep } from "lodash";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
@@ -127,7 +128,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||
? databaseAccount?.location
|
||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
|
||||
const disallowedLocationsUri: string = useNewPortalBackendEndpoint(Constants.BackendApi.DisallowedLocations)
|
||||
? `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`
|
||||
: `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
try {
|
||||
const response = await fetch(disallowedLocationsUri, {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React from "react";
|
||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||
@@ -57,19 +56,6 @@ function openCollectionTab(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
configContext.platform === Platform.Fabric &&
|
||||
!(
|
||||
// whitelist the tab kinds that are allowed to be opened in Fabric
|
||||
(
|
||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery
|
||||
)
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//expand database first if not expanded to load the collections
|
||||
if (!database.isDatabaseExpanded?.()) {
|
||||
database.expandDatabase?.();
|
||||
@@ -135,28 +121,10 @@ function openCollectionTab(
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||
) {
|
||||
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||
collection.onNewQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
||||
openQueryTabAction.splitterDirection,
|
||||
openQueryTabAction.queryViewSizePercent,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.MongoQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoQuery]
|
||||
) {
|
||||
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||
collection.onNewMongoQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
||||
openQueryTabAction.splitterDirection,
|
||||
openQueryTabAction.queryViewSizePercent,
|
||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -819,9 +819,22 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{this.shouldShowAnalyticalStoreOptions() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{this.getAnalyticalStorageContent()}
|
||||
</Text>
|
||||
<Stack horizontal>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Analytical store
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getAnalyticalStorageTooltipContent()}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
@@ -865,7 +878,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Link
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
||||
className="capacitycalculator-link"
|
||||
>
|
||||
Learn more
|
||||
@@ -1218,16 +1230,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return "";
|
||||
}
|
||||
|
||||
private getAnalyticalStorageContent(): JSX.Element {
|
||||
private getAnalyticalStorageTooltipContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without
|
||||
impacting the performance of transactional workloads.{" "}
|
||||
<Link
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
||||
target="_blank"
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
>
|
||||
<Link target="_blank" href="https://aka.ms/analytical-store-overview">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
@@ -94,7 +94,6 @@
|
||||
padding-left: @MediumSpace;
|
||||
|
||||
.paneErrorLink {
|
||||
color: @LinkColor;
|
||||
cursor: pointer;
|
||||
font-size: @mediumFontSize;
|
||||
}
|
||||
|
||||
@@ -174,26 +174,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
const styles = useStyles();
|
||||
|
||||
const explorerVersion = configContext.gitSha;
|
||||
const isEmulator = configContext.platform === Platform.Emulator;
|
||||
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
||||
const showRetrySettings =
|
||||
(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") &&
|
||||
!isEmulator;
|
||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator;
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const showEnableEntraIdRbac =
|
||||
userContext.apiType === "SQL" &&
|
||||
userContext.authType === AuthType.AAD &&
|
||||
configContext.platform !== Platform.Fabric &&
|
||||
!isEmulator;
|
||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
|
||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
||||
const shouldShowCopilotSampleDBOption =
|
||||
userContext.apiType === "SQL" &&
|
||||
useQueryCopilot.getState().copilotEnabled &&
|
||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||
!isEmulator;
|
||||
|
||||
useDatabases.getState().sampleDataResourceTokenCollection;
|
||||
const handlerOnSubmit = async () => {
|
||||
setIsExecuting(true);
|
||||
|
||||
@@ -204,17 +193,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||
|
||||
if (
|
||||
enableDataPlaneRBACOption !== LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) ||
|
||||
retryAttempts !== LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts) ||
|
||||
retryInterval !== LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval) ||
|
||||
MaxWaitTimeInSeconds !== LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds)
|
||||
) {
|
||||
updateUserContext({
|
||||
refreshCosmosClient: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
|
||||
if (
|
||||
@@ -224,6 +202,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
) {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: true,
|
||||
hasDataPlaneRbacSettingChanged: true,
|
||||
});
|
||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||
try {
|
||||
@@ -247,6 +226,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
} else {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: false,
|
||||
hasDataPlaneRbacSettingChanged: true,
|
||||
});
|
||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
|
||||
@@ -502,7 +482,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
return (
|
||||
<RightPaneForm {...genericPaneProps}>
|
||||
<div className={`paneMainContent ${styles.container}`}>
|
||||
<Accordion className={`customAccordion ${styles.firstItem}`}>
|
||||
<Accordion className={styles.firstItem}>
|
||||
{shouldShowQueryPageOptions && (
|
||||
<AccordionItem value="1">
|
||||
<AccordionHeader>
|
||||
@@ -552,37 +532,40 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{showEnableEntraIdRbac && (
|
||||
<AccordionItem value="2">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID
|
||||
RBAC.
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
</a>
|
||||
{userContext.apiType === "SQL" &&
|
||||
userContext.authType === AuthType.AAD &&
|
||||
configContext.platform !== Platform.Fabric && (
|
||||
<AccordionItem value="2">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||
ID RBAC.
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
</a>
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||
options={dataPlaneRBACOptionsList}
|
||||
styles={choiceButtonStyles}
|
||||
selectedKey={enableDataPlaneRBACOption}
|
||||
onChange={handleOnDataPlaneRBACOptionChange}
|
||||
/>
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||
options={dataPlaneRBACOptionsList}
|
||||
styles={choiceButtonStyles}
|
||||
selectedKey={enableDataPlaneRBACOption}
|
||||
onChange={handleOnDataPlaneRBACOptionChange}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{userContext.apiType === "SQL" && !isEmulator && (
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
<>
|
||||
<AccordionItem value="3">
|
||||
<AccordionHeader>
|
||||
@@ -680,103 +663,102 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionItem>
|
||||
</>
|
||||
)}
|
||||
{showRetrySettings && (
|
||||
<AccordionItem value="6">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Retry Settings</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Retry policy associated with throttled requests during CosmosDB queries.
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max retry attempts</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max number of retries to be performed for a request. Default value 9.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + retryAttempts}
|
||||
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
||||
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
||||
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned
|
||||
as part of the response. Default value is 0 milliseconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1000}
|
||||
step={1000}
|
||||
value={"" + retryInterval}
|
||||
onChange={handleOnRetryIntervalSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
||||
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
||||
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max wait time (s)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||
seconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + MaxWaitTimeInSeconds}
|
||||
onChange={handleOnMaxWaitTimeSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
||||
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
||||
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
|
||||
<AccordionItem value="6">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Retry Settings</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Retry policy associated with throttled requests during CosmosDB queries.
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{!isEmulator && (
|
||||
<AccordionItem value="7">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable container pagination</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable container pagination"
|
||||
checked={containerPaginationEnabled}
|
||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||
label="Enable container pagination"
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max retry attempts</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max number of retries to be performed for a request. Default value 9.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + retryAttempts}
|
||||
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
||||
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
||||
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as
|
||||
part of the response. Default value is 0 milliseconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1000}
|
||||
step={1000}
|
||||
value={"" + retryInterval}
|
||||
onChange={handleOnRetryIntervalSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
||||
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
||||
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max wait time (s)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||
seconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + MaxWaitTimeInSeconds}
|
||||
onChange={handleOnMaxWaitTimeSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
||||
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
||||
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="7">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable container pagination</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable container pagination"
|
||||
checked={containerPaginationEnabled}
|
||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||
label="Enable container pagination"
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
{shouldShowCrossPartitionOption && (
|
||||
<AccordionItem value="8">
|
||||
<AccordionHeader>
|
||||
@@ -802,6 +784,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowParallelismOption && (
|
||||
<AccordionItem value="9">
|
||||
<AccordionHeader>
|
||||
@@ -835,6 +818,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowPriorityLevelOption && (
|
||||
<AccordionItem value="10">
|
||||
<AccordionHeader>
|
||||
@@ -858,6 +842,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowGraphAutoVizOption && (
|
||||
<AccordionItem value="11">
|
||||
<AccordionHeader>
|
||||
@@ -879,6 +864,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowCopilotSampleDBOption && (
|
||||
<AccordionItem value="12">
|
||||
<AccordionHeader>
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
||||
>
|
||||
<Accordion
|
||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||
className="___1uf6361_0000000 fz7g6wx"
|
||||
>
|
||||
<AccordionItem
|
||||
value="1"
|
||||
@@ -572,7 +572,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
||||
>
|
||||
<Accordion
|
||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||
className="___1uf6361_0000000 fz7g6wx"
|
||||
>
|
||||
<AccordionItem
|
||||
value="6"
|
||||
|
||||
@@ -309,24 +309,40 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
|
||||
|
||||
<StyledLinkBase
|
||||
aria-label="Learn more about analytical store."
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
Analytical store
|
||||
</Text>
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content={
|
||||
<Text
|
||||
variant="small"
|
||||
>
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
}
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
@@ -384,7 +400,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
. Enable Synapse Link for this Cosmos DB account.
|
||||
|
||||
<StyledLinkBase
|
||||
aria-label="Learn more about Azure Synapse Link."
|
||||
className="capacitycalculator-link"
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
|
||||
@@ -75,7 +75,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
const inputEdited = useRef(false);
|
||||
const itemRefs = useRef([]);
|
||||
const searchInputRef = useRef(null);
|
||||
const copyQueryRef = useRef(null);
|
||||
const {
|
||||
openFeedbackModal,
|
||||
hideFeedbackModalForLikedQueries,
|
||||
@@ -133,7 +132,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
document.body.removeChild(queryElement);
|
||||
|
||||
setshowCopyPopup(true);
|
||||
copyQueryRef.current.focus();
|
||||
setTimeout(() => {
|
||||
setshowCopyPopup(false);
|
||||
}, 6000);
|
||||
@@ -307,7 +305,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
if (isGeneratingQuery === null) {
|
||||
return " ";
|
||||
} else if (isGeneratingQuery) {
|
||||
return "Thinking";
|
||||
return "Content is loading";
|
||||
} else {
|
||||
return "Content is updated";
|
||||
}
|
||||
@@ -402,7 +400,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
allowDisabledFocus={true}
|
||||
style={{ background: "none" }}
|
||||
onClick={() => startGenerateQueryProcess()}
|
||||
aria-label="Send"
|
||||
@@ -679,7 +676,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
)}
|
||||
<CommandBarButton
|
||||
className="copyQuery"
|
||||
elementRef={copyQueryRef}
|
||||
onClick={copyGeneratedCode}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
|
||||
@@ -710,9 +706,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{(showFeedbackBar || isGeneratingQuery) && (
|
||||
<span role="alert" className="screenReaderOnly" aria-label={getAriaLabel()} />
|
||||
)}
|
||||
{isGeneratingQuery && (
|
||||
<ProgressIndicator
|
||||
label="Thinking..."
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
Areas,
|
||||
BackendApi,
|
||||
ConnectionStatusType,
|
||||
ContainerStatusType,
|
||||
HttpStatusCodes,
|
||||
@@ -31,6 +32,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@@ -80,7 +82,9 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
|
||||
};
|
||||
|
||||
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||
const backendEndpoint: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
|
||||
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
|
||||
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
|
||||
@@ -26,7 +26,7 @@ import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
||||
import { Allotment, AllotmentHandle } from "allotment";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { debounce } from "lodash";
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
const useSidebarStyles = makeStyles({
|
||||
sidebar: {
|
||||
@@ -109,7 +109,6 @@ interface GlobalCommand {
|
||||
icon: JSX.Element;
|
||||
onClick: () => void;
|
||||
keyboardAction?: KeyboardAction;
|
||||
ref?: React.RefObject<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
@@ -119,7 +118,6 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
// However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu.
|
||||
// We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render.
|
||||
const [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null);
|
||||
const primaryFocusableRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const actions = useMemo<GlobalCommand[]>(() => {
|
||||
if (
|
||||
@@ -179,16 +177,6 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
);
|
||||
}, [actions, setKeyboardActions]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (primaryFocusableRef.current) {
|
||||
const timer = setTimeout(() => {
|
||||
primaryFocusableRef.current.focus();
|
||||
}, 0);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
if (!primaryAction) {
|
||||
return null;
|
||||
}
|
||||
@@ -196,7 +184,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
return (
|
||||
<div className={styles.globalCommandsContainer} data-test="GlobalCommands">
|
||||
{actions.length === 1 ? (
|
||||
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick} ref={primaryFocusableRef}>
|
||||
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick}>
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
) : (
|
||||
@@ -206,7 +194,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
<div ref={setGlobalCommandButton}>
|
||||
<SplitButton
|
||||
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
||||
primaryActionButton={{ onClick: onPrimaryActionClick, ref: primaryFocusableRef }}
|
||||
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
||||
className={styles.globalCommandsSplitButton}
|
||||
icon={primaryAction.icon}
|
||||
>
|
||||
|
||||
@@ -39,7 +39,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||
role="button"
|
||||
>
|
||||
<div>
|
||||
<img src={imgSrc} alt={title} aria-hidden="true" />
|
||||
<img src={imgSrc} />
|
||||
</div>
|
||||
<Stack style={{ marginLeft: 16 }}>
|
||||
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { CassandraProxyAPIs } from "../../Common/Constants";
|
||||
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
|
||||
import { handleError } from "../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
@@ -264,6 +264,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string,
|
||||
): Promise<Entities.IListTableEntitiesResult> {
|
||||
if (!this.useCassandraProxyEndpoint("postQuery")) {
|
||||
return this.queryDocuments_ToBeDeprecated(collection, query, shouldNotify, paginationToken);
|
||||
}
|
||||
const clearMessage =
|
||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||
try {
|
||||
@@ -306,6 +309,55 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
}
|
||||
|
||||
public async queryDocuments_ToBeDeprecated(
|
||||
collection: ViewModels.Collection,
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string,
|
||||
): Promise<Entities.IListTableEntitiesResult> {
|
||||
const clearMessage =
|
||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||
try {
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestQueryApi
|
||||
: Constants.CassandraBackend.queryApi;
|
||||
const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
query,
|
||||
paginationToken,
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
});
|
||||
shouldNotify &&
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`,
|
||||
);
|
||||
return {
|
||||
Results: data.result,
|
||||
ContinuationToken: data.paginationToken,
|
||||
};
|
||||
} catch (error) {
|
||||
shouldNotify &&
|
||||
handleError(
|
||||
error,
|
||||
"QueryDocuments_ToBeDeprecated_Cassandra",
|
||||
`Failed to query rows for table ${collection.id()}`,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage?.();
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
entitiesToDelete: Entities.ITableEntity[],
|
||||
@@ -419,6 +471,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!this.useCassandraProxyEndpoint("getKeys")) {
|
||||
return this.getTableKeys_ToBeDeprecated(collection);
|
||||
}
|
||||
|
||||
if (!!collection.cassandraKeys) {
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
}
|
||||
@@ -459,7 +515,52 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableKeys_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!!collection.cassandraKeys) {
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
}
|
||||
const clearInProgressMessage = logConsoleProgress(`Fetching keys for table ${collection.id()}`);
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestKeysApi
|
||||
: Constants.CassandraBackend.keysApi;
|
||||
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKeys>();
|
||||
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
})
|
||||
.then(
|
||||
(data: CassandraTableKeys) => {
|
||||
collection.cassandraKeys = data;
|
||||
logConsoleInfo(`Successfully fetched keys for table ${collection.id()}`);
|
||||
deferred.resolve(data);
|
||||
},
|
||||
(error: any) => {
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.done(clearInProgressMessage);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
if (!this.useCassandraProxyEndpoint("getSchema")) {
|
||||
return this.getTableSchema_ToBeDeprecated(collection);
|
||||
}
|
||||
|
||||
if (!!collection.cassandraSchema) {
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
}
|
||||
@@ -501,7 +602,52 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableSchema_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
if (!!collection.cassandraSchema) {
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
}
|
||||
const clearInProgressMessage = logConsoleProgress(`Fetching schema for table ${collection.id()}`);
|
||||
const { databaseAccount, authType } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestSchemaApi
|
||||
: Constants.CassandraBackend.schemaApi;
|
||||
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKey[]>();
|
||||
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
})
|
||||
.then(
|
||||
(data: any) => {
|
||||
collection.cassandraSchema = data.columns;
|
||||
logConsoleInfo(`Successfully fetched schema for table ${collection.id()}`);
|
||||
deferred.resolve(data.columns);
|
||||
},
|
||||
(error: any) => {
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.done(clearInProgressMessage);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
|
||||
if (!this.useCassandraProxyEndpoint("createOrDelete")) {
|
||||
return this.createOrDeleteQuery_ToBeDeprecated(cassandraEndpoint, resourceId, query);
|
||||
}
|
||||
|
||||
const deferred = Q.defer();
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
@@ -531,6 +677,38 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private createOrDeleteQuery_ToBeDeprecated(
|
||||
cassandraEndpoint: string,
|
||||
resourceId: string,
|
||||
query: string,
|
||||
): Q.Promise<any> {
|
||||
const deferred = Q.defer();
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestCreateOrDeleteApi
|
||||
: Constants.CassandraBackend.createOrDeleteApi;
|
||||
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
||||
resourceId: resourceId,
|
||||
query: query,
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
}).then(
|
||||
(data: any) => {
|
||||
deferred.resolve();
|
||||
},
|
||||
(reason) => {
|
||||
deferred.reject(reason);
|
||||
},
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private trimCassandraEndpoint(cassandraEndpoint: string): string {
|
||||
if (!cassandraEndpoint) {
|
||||
return cassandraEndpoint;
|
||||
@@ -569,4 +747,23 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
||||
return collection.cassandraKeys.partitionKeys[0].property;
|
||||
}
|
||||
|
||||
private useCassandraProxyEndpoint(api: string): boolean {
|
||||
const activeCassandraProxyEndpoints: string[] = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
CassandraProxyEndpoints.Fairfax,
|
||||
CassandraProxyEndpoints.Mooncake,
|
||||
];
|
||||
|
||||
if (configContext.globallyEnabledCassandraAPIs.includes(api)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,17 @@
|
||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||
import {
|
||||
AppStateComponentNames,
|
||||
deleteSubComponentState,
|
||||
readSubComponentState,
|
||||
saveSubComponentState,
|
||||
deleteState,
|
||||
loadState,
|
||||
saveState,
|
||||
saveStateDebounced,
|
||||
} from "Shared/AppStatePersistenceUtility";
|
||||
import { userContext } from "UserContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
const componentName = AppStateComponentNames.DocumentsTab;
|
||||
|
||||
export enum SubComponentName {
|
||||
ColumnSizes = "ColumnSizes",
|
||||
@@ -15,7 +21,6 @@ export enum SubComponentName {
|
||||
MainTabDivider = "MainTabDivider",
|
||||
ColumnsSelection = "ColumnsSelection",
|
||||
ColumnSort = "ColumnSort",
|
||||
CurrentFilter = "CurrentFilter",
|
||||
}
|
||||
|
||||
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
||||
@@ -25,22 +30,84 @@ export type TabDivider = { leftPaneWidthPercent: number };
|
||||
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
||||
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
||||
|
||||
// Wrap the ...SubComponentState functions for type safety
|
||||
|
||||
export const readDocumentsTabSubComponentState = <T>(
|
||||
/**
|
||||
*
|
||||
* @param subComponentName
|
||||
* @param collection
|
||||
* @param defaultValue Will be returned if persisted state is not found
|
||||
* @returns
|
||||
*/
|
||||
export const readSubComponentState = <T>(
|
||||
subComponentName: SubComponentName,
|
||||
collection: ViewModels.CollectionBase,
|
||||
defaultValue: T,
|
||||
): T => readSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, defaultValue);
|
||||
): T => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export const saveDocumentsTabSubComponentState = <T>(
|
||||
const state = loadState({
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection.databaseId,
|
||||
containerName: collection.id(),
|
||||
}) as T;
|
||||
|
||||
return state || defaultValue;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param subComponentName
|
||||
* @param collection
|
||||
* @param state State to save
|
||||
* @param debounce true for high-frequency calls (e.g mouse drag events)
|
||||
*/
|
||||
export const saveSubComponentState = <T>(
|
||||
subComponentName: SubComponentName,
|
||||
collection: ViewModels.CollectionBase,
|
||||
state: T,
|
||||
debounce?: boolean,
|
||||
): void => saveSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, state, debounce);
|
||||
): void => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
|
||||
return;
|
||||
}
|
||||
|
||||
export const deleteDocumentsTabSubComponentState = (
|
||||
subComponentName: SubComponentName,
|
||||
collection: ViewModels.CollectionBase,
|
||||
) => deleteSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection);
|
||||
(debounce ? saveStateDebounced : saveState)(
|
||||
{
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection.databaseId,
|
||||
containerName: collection.id(),
|
||||
},
|
||||
state,
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteSubComponentState = (subComponentName: SubComponentName, collection: ViewModels.CollectionBase) => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
|
||||
return;
|
||||
}
|
||||
|
||||
deleteState({
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection.databaseId,
|
||||
containerName: collection.id(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
||||
import { readDocument } from "Common/dataAccess/readDocument";
|
||||
import { updateDocument } from "Common/dataAccess/updateDocument";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
@@ -35,9 +34,8 @@ import {
|
||||
FilterHistory,
|
||||
SubComponentName,
|
||||
TabDivider,
|
||||
deleteDocumentsTabSubComponentState,
|
||||
readDocumentsTabSubComponentState,
|
||||
saveDocumentsTabSubComponentState,
|
||||
readSubComponentState,
|
||||
saveSubComponentState,
|
||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||
@@ -142,8 +140,6 @@ export class DocumentsTabV2 extends TabsBase {
|
||||
private title: string;
|
||||
private resourceTokenPartitionKey: string;
|
||||
|
||||
protected persistedState: OpenCollectionTab;
|
||||
|
||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||
super(options);
|
||||
|
||||
@@ -151,13 +147,6 @@ export class DocumentsTabV2 extends TabsBase {
|
||||
this.title = options.title;
|
||||
this.partitionKey = options.partitionKey;
|
||||
this.resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
||||
|
||||
this.persistedState = {
|
||||
actionType: ActionType.OpenCollectionTab,
|
||||
tabKind: options.isPreferredApiMongoDB ? TabKind.MongoDocuments : TabKind.SQLDocuments,
|
||||
databaseResourceId: options.collection.databaseId,
|
||||
collectionResourceId: options.collection.id(),
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
@@ -586,10 +575,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
onIsExecutingChange,
|
||||
isTabActive,
|
||||
}): JSX.Element => {
|
||||
const [filterContent, setFilterContent] = useState<string>(() =>
|
||||
readDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, ""),
|
||||
);
|
||||
|
||||
const [filterContent, setFilterContent] = useState<string>("");
|
||||
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
const styles = useDocumentsTabStyles();
|
||||
@@ -620,7 +606,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
// State
|
||||
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
||||
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||
readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||
leftPaneWidthPercent: 35,
|
||||
}),
|
||||
);
|
||||
@@ -635,7 +621,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
// User's filter history
|
||||
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
||||
readDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||
);
|
||||
|
||||
// For progress bar for bulk delete (noSql)
|
||||
@@ -777,7 +763,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
};
|
||||
|
||||
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
||||
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
||||
SubComponentName.ColumnsSelection,
|
||||
_collection,
|
||||
undefined,
|
||||
@@ -822,7 +808,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
useEffect(() => {
|
||||
setKeyboardActions({
|
||||
[KeyboardAction.CLEAR_SEARCH]: () => {
|
||||
updateFilterContent("");
|
||||
setFilterContent("");
|
||||
refreshDocumentsGrid(true);
|
||||
return true;
|
||||
},
|
||||
@@ -1150,16 +1136,27 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
|
||||
}
|
||||
} else {
|
||||
deletePromise = MongoProxyClient.deleteDocuments(
|
||||
_collection.databaseId,
|
||||
_collection as ViewModels.Collection,
|
||||
toDeleteDocumentIds,
|
||||
).then(({ deletedCount, isAcknowledged }) => {
|
||||
if (deletedCount === toDeleteDocumentIds.length && isAcknowledged) {
|
||||
return toDeleteDocumentIds;
|
||||
}
|
||||
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
||||
});
|
||||
if (isMongoBulkDeleteDisabled) {
|
||||
// TODO: Once new mongo proxy is available for all users, remove the call for MongoProxyClient.deleteDocument().
|
||||
// MongoProxyClient.deleteDocuments() should be called for all users.
|
||||
deletePromise = MongoProxyClient.deleteDocument(
|
||||
_collection.databaseId,
|
||||
_collection as ViewModels.Collection,
|
||||
toDeleteDocumentIds[0],
|
||||
).then(() => [toDeleteDocumentIds[0]]);
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
} else {
|
||||
deletePromise = MongoProxyClient.deleteDocuments(
|
||||
_collection.databaseId,
|
||||
_collection as ViewModels.Collection,
|
||||
toDeleteDocumentIds,
|
||||
).then(({ deletedCount, isAcknowledged }) => {
|
||||
if (deletedCount === toDeleteDocumentIds.length && isAcknowledged) {
|
||||
return toDeleteDocumentIds;
|
||||
}
|
||||
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return deletePromise
|
||||
@@ -1648,7 +1645,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
||||
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
||||
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>(
|
||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
||||
SubComponentName.ColumnsSelection,
|
||||
_collection,
|
||||
undefined,
|
||||
@@ -1959,7 +1956,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
||||
|
||||
setLastFilterContents(limitedLastFilterContents);
|
||||
saveDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||
saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||
};
|
||||
|
||||
const refreshDocumentsGrid = useCallback(
|
||||
@@ -2016,7 +2013,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
setSelectedColumnIds(newSelectedColumnIds);
|
||||
|
||||
saveDocumentsTabSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
||||
saveSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
||||
selectedColumnIds: newSelectedColumnIds,
|
||||
columnDefinitions,
|
||||
});
|
||||
@@ -2043,8 +2040,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}
|
||||
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]);
|
||||
|
||||
// TODO: remove isMongoBulkDeleteDisabled when new mongo proxy is enabled for all users
|
||||
// TODO: remove partitionKey.systemKey when JS SDK bug is fixed
|
||||
const isBulkDeleteDisabled = partitionKey.systemKey && !isPreferredApiMongoDB;
|
||||
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint(Constants.MongoProxyApi.BulkDelete);
|
||||
const isBulkDeleteDisabled =
|
||||
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
||||
// -------------------------------------------------------
|
||||
|
||||
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
|
||||
@@ -2063,15 +2063,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
return options;
|
||||
};
|
||||
|
||||
const updateFilterContent = (filter: string): void => {
|
||||
if (filter === "" || filter === undefined) {
|
||||
deleteDocumentsTabSubComponentState(SubComponentName.CurrentFilter, _collection);
|
||||
} else {
|
||||
saveDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, filter, true);
|
||||
}
|
||||
setFilterContent(filter);
|
||||
};
|
||||
|
||||
return (
|
||||
<CosmosFluentProvider className={styles.container}>
|
||||
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
||||
@@ -2086,7 +2077,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}
|
||||
title="Type a query predicate or choose one from the list."
|
||||
value={filterContent}
|
||||
onChange={updateFilterContent}
|
||||
onChange={(value) => setFilterContent(value)}
|
||||
onKeyDown={onFilterKeyDown}
|
||||
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
|
||||
/>
|
||||
@@ -2112,7 +2103,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
<Allotment
|
||||
onDragEnd={(sizes: number[]) => {
|
||||
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||
saveDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||
setTabStateData(tabStateData);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -42,9 +42,9 @@ import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPan
|
||||
import {
|
||||
ColumnSizesMap,
|
||||
ColumnSort,
|
||||
deleteDocumentsTabSubComponentState,
|
||||
readDocumentsTabSubComponentState,
|
||||
saveDocumentsTabSubComponentState,
|
||||
deleteSubComponentState,
|
||||
readSubComponentState,
|
||||
saveSubComponentState,
|
||||
SubComponentName,
|
||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||
@@ -118,11 +118,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
const sortedRowsRef = React.useRef(null);
|
||||
|
||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
||||
const columnSizesMap: ColumnSizesMap = readDocumentsTabSubComponentState(
|
||||
SubComponentName.ColumnSizes,
|
||||
collection,
|
||||
{},
|
||||
);
|
||||
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
||||
const columnSizesPx: TableColumnSizingOptions = {};
|
||||
selectedColumnIds.forEach((columnId) => {
|
||||
if (
|
||||
@@ -146,7 +142,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
sortDirection: "ascending" | "descending";
|
||||
sortColumn: TableColumnId | undefined;
|
||||
}>(() => {
|
||||
const sort = readDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
||||
const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
||||
|
||||
if (!sort) {
|
||||
return {
|
||||
@@ -178,12 +174,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
return acc;
|
||||
}, {} as ColumnSizesMap);
|
||||
|
||||
saveDocumentsTabSubComponentState<ColumnSizesMap>(
|
||||
SubComponentName.ColumnSizes,
|
||||
collection,
|
||||
persistentSizes,
|
||||
true,
|
||||
);
|
||||
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
||||
|
||||
return newSizingOptions;
|
||||
});
|
||||
@@ -195,14 +186,11 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
setColumnSort(event, columnId, direction);
|
||||
|
||||
if (columnId === undefined || direction === undefined) {
|
||||
deleteDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection);
|
||||
deleteSubComponentState(SubComponentName.ColumnSort, collection);
|
||||
return;
|
||||
}
|
||||
|
||||
saveDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, {
|
||||
columnId,
|
||||
direction,
|
||||
});
|
||||
saveSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { columnId, direction });
|
||||
};
|
||||
|
||||
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ActionType, TabKind } from "Contracts/ActionContracts";
|
||||
import React from "react";
|
||||
import MongoUtility from "../../../Common/MongoUtility";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
@@ -21,7 +20,7 @@ export class NewMongoQueryTab extends NewQueryTab {
|
||||
private mongoQueryTabProps: IMongoQueryTabProps,
|
||||
) {
|
||||
super(options, mongoQueryTabProps);
|
||||
this.queryText = options.queryText ?? "";
|
||||
this.queryText = "";
|
||||
this.iMongoQueryTabComponentProps = {
|
||||
collection: options.collection,
|
||||
isExecutionError: this.isExecutionError(),
|
||||
@@ -29,8 +28,6 @@ export class NewMongoQueryTab extends NewQueryTab {
|
||||
tabsBaseInstance: this,
|
||||
queryText: this.queryText,
|
||||
partitionKey: this.partitionKey,
|
||||
splitterDirection: options.splitterDirection,
|
||||
queryViewSizePercent: options.queryViewSizePercent,
|
||||
container: this.mongoQueryTabProps.container,
|
||||
onTabAccessor: (instance: ITabAccessor): void => {
|
||||
this.iTabAccessor = instance;
|
||||
@@ -38,26 +35,6 @@ export class NewMongoQueryTab extends NewQueryTab {
|
||||
isPreferredApiMongoDB: true,
|
||||
monacoEditorSetting: "plaintext",
|
||||
viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
|
||||
onUpdatePersistedState: (state: {
|
||||
queryText: string;
|
||||
splitterDirection: string;
|
||||
queryViewSizePercent: number;
|
||||
}): void => {
|
||||
this.persistedState = {
|
||||
actionType: ActionType.OpenCollectionTab,
|
||||
tabKind: TabKind.SQLQuery,
|
||||
databaseResourceId: options.collection.databaseId,
|
||||
collectionResourceId: options.collection.id(),
|
||||
query: {
|
||||
text: state.queryText,
|
||||
},
|
||||
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
|
||||
queryViewSizePercent: state.queryViewSizePercent,
|
||||
};
|
||||
if (this.triggerPersistState) {
|
||||
this.triggerPersistState();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMongoProxyEndpoint } from "Common/MongoProxyClient";
|
||||
import React, { Component } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -48,13 +50,15 @@ export default class MongoShellTabComponent extends Component<
|
||||
IMongoShellTabComponentStates
|
||||
> {
|
||||
private _logTraces: Map<string, number>;
|
||||
private _useMongoProxyEndpoint: boolean;
|
||||
|
||||
constructor(props: IMongoShellTabComponentProps) {
|
||||
super(props);
|
||||
this._logTraces = new Map();
|
||||
this._useMongoProxyEndpoint = useMongoProxyEndpoint(Constants.MongoProxyApi.LegacyMongoShell);
|
||||
|
||||
this.state = {
|
||||
url: getMongoShellUrl(),
|
||||
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||
};
|
||||
|
||||
props.onMongoShellTabAccessor({
|
||||
@@ -109,9 +113,17 @@ export default class MongoShellTabComponent extends Component<
|
||||
const resourceId = databaseAccount?.id;
|
||||
const accountName = databaseAccount?.name;
|
||||
const documentEndpoint = databaseAccount?.properties.mongoEndpoint || databaseAccount?.properties.documentEndpoint;
|
||||
const mongoEndpoint =
|
||||
documentEndpoint.substr(
|
||||
Constants.MongoDBAccounts.protocol.length + 3,
|
||||
documentEndpoint.length -
|
||||
(Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length),
|
||||
) + Constants.MongoDBAccounts.defaultPort.toString();
|
||||
const databaseId = this.props.collection.databaseId;
|
||||
const collectionId = this.props.collection.id();
|
||||
const apiEndpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||
const apiEndpoint = this._useMongoProxyEndpoint
|
||||
? configContext.MONGO_PROXY_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
const encryptedAuthToken: string = userContext.accessToken;
|
||||
|
||||
shellIframe.contentWindow.postMessage(
|
||||
@@ -120,7 +132,7 @@ export default class MongoShellTabComponent extends Component<
|
||||
data: {
|
||||
resourceId: resourceId,
|
||||
accountName: accountName,
|
||||
mongoEndpoint: documentEndpoint,
|
||||
mongoEndpoint: this._useMongoProxyEndpoint ? documentEndpoint : mongoEndpoint,
|
||||
authorization: authorization,
|
||||
databaseId: databaseId,
|
||||
collectionId: collectionId,
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Platform, resetConfigContext, updateConfigContext } from "../../../Conf
|
||||
import { updateUserContext, userContext } from "../../../UserContext";
|
||||
import { getMongoShellUrl } from "./getMongoShellUrl";
|
||||
|
||||
const mongoBackendEndpoint = "https://localhost:1234";
|
||||
|
||||
describe("getMongoShellUrl", () => {
|
||||
let queryString = "";
|
||||
|
||||
@@ -9,6 +11,7 @@ describe("getMongoShellUrl", () => {
|
||||
resetConfigContext();
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||
platform: Platform.Hosted,
|
||||
});
|
||||
|
||||
@@ -34,7 +37,12 @@ describe("getMongoShellUrl", () => {
|
||||
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
|
||||
});
|
||||
|
||||
it("should return /index.html by default", () => {
|
||||
expect(getMongoShellUrl().toString()).toContain(`/index.html?${queryString}`);
|
||||
it("should return /indexv2.html by default", () => {
|
||||
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("should return /index.html when useMongoProxyEndpoint is true", () => {
|
||||
const useMongoProxyEndpoint: boolean = true;
|
||||
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { userContext } from "../../../UserContext";
|
||||
|
||||
export function getMongoShellUrl(): string {
|
||||
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string {
|
||||
const { databaseAccount: account } = userContext;
|
||||
const resourceId = account?.id;
|
||||
const accountName = account?.name;
|
||||
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
|
||||
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
|
||||
|
||||
return `/mongoshell/index.html?${queryString}`;
|
||||
return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { ActionType, OpenQueryTab, TabKind } from "Contracts/ActionContracts";
|
||||
import { MessageTypes } from "Contracts/MessageTypes";
|
||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||
import { userContext } from "UserContext";
|
||||
@@ -27,8 +26,6 @@ export class NewQueryTab extends TabsBase {
|
||||
public iQueryTabComponentProps: IQueryTabComponentProps;
|
||||
public iTabAccessor: ITabAccessor;
|
||||
|
||||
protected persistedState: OpenQueryTab;
|
||||
|
||||
constructor(
|
||||
options: QueryTabOptions,
|
||||
private props: IQueryTabProps,
|
||||
@@ -42,41 +39,12 @@ export class NewQueryTab extends TabsBase {
|
||||
tabsBaseInstance: this,
|
||||
queryText: options.queryText,
|
||||
partitionKey: this.partitionKey,
|
||||
splitterDirection: options.splitterDirection,
|
||||
queryViewSizePercent: options.queryViewSizePercent,
|
||||
container: this.props.container,
|
||||
onTabAccessor: (instance: ITabAccessor): void => {
|
||||
this.iTabAccessor = instance;
|
||||
},
|
||||
isPreferredApiMongoDB: false,
|
||||
onUpdatePersistedState: (state: {
|
||||
queryText: string;
|
||||
splitterDirection: string;
|
||||
queryViewSizePercent: number;
|
||||
}): void => {
|
||||
this.persistedState = {
|
||||
actionType: ActionType.OpenCollectionTab,
|
||||
tabKind: TabKind.SQLQuery,
|
||||
databaseResourceId: options.collection.databaseId,
|
||||
collectionResourceId: options.collection.id(),
|
||||
query: {
|
||||
text: state.queryText,
|
||||
},
|
||||
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
|
||||
queryViewSizePercent: state.queryViewSizePercent,
|
||||
};
|
||||
if (this.triggerPersistState) {
|
||||
this.triggerPersistState();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// set initial state
|
||||
this.iQueryTabComponentProps.onUpdatePersistedState({
|
||||
queryText: options.queryText,
|
||||
splitterDirection: options.splitterDirection,
|
||||
queryViewSizePercent: options.queryViewSizePercent,
|
||||
});
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
|
||||
@@ -34,7 +34,6 @@ jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
||||
AppStateComponentNames: {
|
||||
QueryCopilot: "QueryCopilot",
|
||||
},
|
||||
readSubComponentState: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("QueryTabComponent", () => {
|
||||
|
||||
@@ -18,7 +18,13 @@ import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||
import {
|
||||
LocalStorageUtility,
|
||||
StorageKey,
|
||||
getDefaultQueryResultsView,
|
||||
getRUThreshold,
|
||||
ruThresholdEnabled,
|
||||
} from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { Allotment } from "allotment";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
@@ -93,13 +99,6 @@ export interface IQueryTabComponentProps {
|
||||
copilotEnabled?: boolean;
|
||||
isSampleCopilotActive?: boolean;
|
||||
copilotStore?: Partial<QueryCopilotState>;
|
||||
splitterDirection?: "horizontal" | "vertical";
|
||||
queryViewSizePercent?: number;
|
||||
onUpdatePersistedState: (state: {
|
||||
queryText: string;
|
||||
splitterDirection: "vertical" | "horizontal";
|
||||
queryViewSizePercent: number;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
interface IQueryTabStates {
|
||||
@@ -119,13 +118,11 @@ interface IQueryTabStates {
|
||||
queryResultsView: SplitterDirection;
|
||||
errors?: QueryError[];
|
||||
modelMarkers?: monaco.editor.IMarkerData[];
|
||||
queryViewSizePercent: number;
|
||||
}
|
||||
|
||||
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
|
||||
const styles = useQueryTabStyles();
|
||||
const copilotStore = useCopilotStore();
|
||||
|
||||
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||
const queryTabProps = {
|
||||
...props,
|
||||
@@ -135,12 +132,12 @@ export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any =>
|
||||
isSampleCopilotActive: isSampleCopilotActive,
|
||||
copilotStore: copilotStore,
|
||||
};
|
||||
return <QueryTabComponentImpl styles={styles} {...queryTabProps} />;
|
||||
return <QueryTabComponentImpl styles={styles} {...queryTabProps}></QueryTabComponentImpl>;
|
||||
};
|
||||
|
||||
export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
|
||||
const styles = useQueryTabStyles();
|
||||
return <QueryTabComponentImpl styles={styles} {...{ ...props }} />;
|
||||
return <QueryTabComponentImpl styles={styles} {...props}></QueryTabComponentImpl>;
|
||||
};
|
||||
|
||||
type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
||||
@@ -149,8 +146,6 @@ type QueryTabComponentImplProps = IQueryTabComponentProps & {
|
||||
|
||||
// Inner (legacy) class component. We only use this component via one of the two functional components above (since we need to use the `useQueryTabStyles` hook).
|
||||
class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps, IQueryTabStates> {
|
||||
private static readonly DEBOUNCE_DELAY_MS = 1000;
|
||||
|
||||
public queryEditorId: string;
|
||||
public executeQueryButton: Button;
|
||||
public saveQueryButton: Button;
|
||||
@@ -162,10 +157,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
private _iterator: MinimalQueryIterator;
|
||||
private queryAbortController: AbortController;
|
||||
queryEditor: React.RefObject<EditorReact>;
|
||||
private timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
constructor(props: QueryTabComponentImplProps) {
|
||||
super(props);
|
||||
|
||||
this.queryEditor = createRef<EditorReact>();
|
||||
|
||||
this.state = {
|
||||
@@ -181,9 +176,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
cancelQueryTimeoutID: undefined,
|
||||
copilotActive: this._queryCopilotActive(),
|
||||
currentTabActive: true,
|
||||
queryResultsView:
|
||||
props.splitterDirection === "vertical" ? SplitterDirection.Vertical : SplitterDirection.Horizontal,
|
||||
queryViewSizePercent: props.queryViewSizePercent,
|
||||
queryResultsView: getDefaultQueryResultsView(),
|
||||
};
|
||||
this.isCloseClicked = false;
|
||||
this.splitterId = this.props.tabId + "_splitter";
|
||||
@@ -214,23 +207,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to save the query text in the query tab state
|
||||
* Since it reads and writes to the same state, it is debounced
|
||||
*/
|
||||
private saveQueryTabStateDebounced = () => {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
this.timeoutId = setTimeout(async () => {
|
||||
this.props.onUpdatePersistedState({
|
||||
queryText: this.state.sqlQueryEditorContent,
|
||||
splitterDirection: this.state.queryResultsView,
|
||||
queryViewSizePercent: this.state.queryViewSizePercent,
|
||||
});
|
||||
}, QueryTabComponentImpl.DEBOUNCE_DELAY_MS);
|
||||
};
|
||||
|
||||
private _queryCopilotActive(): boolean {
|
||||
if (this.props.copilotEnabled) {
|
||||
return readCopilotToggleStatus(userContext.databaseAccount);
|
||||
@@ -375,7 +351,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
ruCapPerOperation: ruThreshold,
|
||||
} as QueryOperationOptions;
|
||||
}
|
||||
|
||||
const queryDocuments = async (firstItemIndex: number) =>
|
||||
await queryDocumentsPage(
|
||||
this.props.collection && this.props.collection.id(),
|
||||
@@ -592,7 +567,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
};
|
||||
}
|
||||
private _setViewLayout(direction: SplitterDirection): void {
|
||||
this.setState({ queryResultsView: direction }, () => this.saveQueryTabStateDebounced());
|
||||
this.setState({ queryResultsView: direction });
|
||||
|
||||
// We'll need to refresh the context buttons to update the selected state of the view buttons
|
||||
setTimeout(() => {
|
||||
@@ -624,16 +599,13 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
if (this.state.copilotActive) {
|
||||
this.props.copilotStore?.setQuery(newContent);
|
||||
}
|
||||
this.setState(
|
||||
{
|
||||
sqlQueryEditorContent: newContent,
|
||||
queryCopilotGeneratedQuery: "",
|
||||
this.setState({
|
||||
sqlQueryEditorContent: newContent,
|
||||
queryCopilotGeneratedQuery: "",
|
||||
|
||||
// Clear the markers when the user edits the document.
|
||||
modelMarkers: [],
|
||||
},
|
||||
() => this.saveQueryTabStateDebounced(),
|
||||
);
|
||||
// Clear the markers when the user edits the document.
|
||||
modelMarkers: [],
|
||||
});
|
||||
if (this.isPreferredApiMongoDB) {
|
||||
if (newContent.length > 0) {
|
||||
this.executeQueryButton = {
|
||||
@@ -732,20 +704,8 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
></QueryCopilotPromptbar>
|
||||
)}
|
||||
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
||||
<Allotment
|
||||
key={vertical.toString()}
|
||||
vertical={vertical}
|
||||
onDragEnd={(sizes: number[]) => {
|
||||
const queryViewSizePercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||
this.setState({ queryViewSizePercent }, () => this.saveQueryTabStateDebounced());
|
||||
}}
|
||||
>
|
||||
<Allotment.Pane
|
||||
data-test="QueryTab/EditorPane"
|
||||
preferredSize={
|
||||
this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined
|
||||
}
|
||||
>
|
||||
<Allotment key={vertical.toString()} vertical={vertical}>
|
||||
<Allotment.Pane data-test="QueryTab/EditorPane">
|
||||
<EditorReact
|
||||
ref={this.queryEditor}
|
||||
className={this.props.styles.queryEditor}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||
import React from "react";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
|
||||
@@ -11,18 +10,6 @@ export class SettingsTabV2 extends TabsBase {
|
||||
}
|
||||
|
||||
export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
||||
protected persistedState: OpenCollectionTab;
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
super(options);
|
||||
this.persistedState = {
|
||||
actionType: ActionType.OpenCollectionTab,
|
||||
tabKind: TabKind.ScaleSettings,
|
||||
databaseResourceId: options.collection.databaseId,
|
||||
collectionResourceId: options.collection.id(),
|
||||
};
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
super.onActivate();
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { IMessageBarStyles, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { IpRule } from "Contracts/DataModels";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
@@ -8,8 +14,10 @@ import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { userContext } from "UserContext";
|
||||
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import ko from "knockout";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
@@ -27,7 +35,11 @@ interface TabsProps {
|
||||
}
|
||||
|
||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
||||
const [
|
||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
|
||||
|
||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS);
|
||||
useEffect(() => {
|
||||
@@ -38,8 +50,51 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
});
|
||||
}, [setKeyboardHandlers]);
|
||||
|
||||
const defaultMessageBarStyles: IMessageBarStyles = {
|
||||
root: {
|
||||
height: `${LayoutConstants.rowHeight}px`,
|
||||
overflow: "hidden",
|
||||
flexDirection: "row",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tabsManagerContainer">
|
||||
{networkSettingsWarning && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.warning}
|
||||
styles={defaultMessageBarStyles}
|
||||
actions={
|
||||
<MessageBarButton
|
||||
onClick={() =>
|
||||
sendMessage({
|
||||
type:
|
||||
userContext.apiType === "VCoreMongo"
|
||||
? MessageTypes.OpenVCoreMongoNetworkingBlade
|
||||
: MessageTypes.OpenPostgresNetworkingBlade,
|
||||
})
|
||||
}
|
||||
>
|
||||
Change network settings
|
||||
</MessageBarButton>
|
||||
}
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
>
|
||||
{networkSettingsWarning}
|
||||
</MessageBar>
|
||||
)}
|
||||
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.warning}
|
||||
styles={defaultMessageBarStyles}
|
||||
onDismiss={() => {
|
||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
||||
}}
|
||||
>
|
||||
{`We have migrated our middleware to new infrastructure. To avoid issues with Data Explorer access, please
|
||||
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
||||
</MessageBar>
|
||||
)}
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
{openedReactTabs.map((tab) => (
|
||||
@@ -173,7 +228,7 @@ const CloseButton = ({
|
||||
onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressClose(undefined, e) : onKeyPressReactTabClose(e, tabKind))}
|
||||
>
|
||||
<span className="tabIcon close-Icon">
|
||||
<img src={errorIcon} title="Close" alt="Close" aria-label="hidden" />
|
||||
<img src={errorIcon} title="Close" alt="Close" role="none" />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
@@ -284,3 +339,57 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||
}
|
||||
};
|
||||
|
||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||
if (
|
||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) ||
|
||||
(userContext.apiType === "Cassandra" &&
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||
ipRules?.length
|
||||
) {
|
||||
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
|
||||
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
|
||||
const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>
|
||||
ipAddressesFromIPRules.includes(legacyPortalBackendIP),
|
||||
);
|
||||
if (!ipRulesIncludeLegacyPortalBackend) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (userContext.apiType === "Mongo") {
|
||||
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
|
||||
configContext.MONGO_PROXY_ENDPOINT,
|
||||
);
|
||||
|
||||
const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint
|
||||
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
||||
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
||||
|
||||
const ipRulesIncludeMongoProxy: boolean = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) =>
|
||||
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
||||
);
|
||||
|
||||
return !ipRulesIncludeMongoProxy;
|
||||
} else if (userContext.apiType === "Cassandra") {
|
||||
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
].includes(configContext.CASSANDRA_PROXY_ENDPOINT);
|
||||
|
||||
const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint
|
||||
? [
|
||||
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac],
|
||||
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod],
|
||||
]
|
||||
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
||||
|
||||
const ipRulesIncludeCassandraProxy: boolean = cassandraProxyOutboundIPs.every(
|
||||
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
||||
);
|
||||
|
||||
return !ipRulesIncludeCassandraProxy;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { OpenTab } from "Contracts/ActionContracts";
|
||||
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -31,8 +30,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
protected _theme: string;
|
||||
public onLoadStartKey: number;
|
||||
|
||||
protected persistedState: OpenTab | undefined = undefined; // Used to store state of tab for persistence
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
super();
|
||||
this.index = options.index;
|
||||
@@ -58,10 +55,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
};
|
||||
}
|
||||
|
||||
// Called by useTabs to persist
|
||||
public getPersistedState = (): OpenTab | null => this.persistedState;
|
||||
public triggerPersistState: () => void = undefined;
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
useTabs.getState().closeTab(this);
|
||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||
|
||||
@@ -630,13 +630,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
}
|
||||
};
|
||||
|
||||
public onNewQueryClick(
|
||||
source: any,
|
||||
event: MouseEvent,
|
||||
queryText?: string,
|
||||
splitterDirection?: "horizontal" | "vertical",
|
||||
queryViewSizePercent?: number,
|
||||
) {
|
||||
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
||||
const collection: ViewModels.Collection = source.collection || source;
|
||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||
const title = "Query " + id;
|
||||
@@ -659,21 +653,13 @@ export default class Collection implements ViewModels.Collection {
|
||||
queryText: queryText,
|
||||
partitionKey: collection.partitionKey,
|
||||
onLoadStartKey: startKey,
|
||||
splitterDirection,
|
||||
queryViewSizePercent,
|
||||
},
|
||||
{ container: this.container },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public onNewMongoQueryClick(
|
||||
source: any,
|
||||
event: MouseEvent,
|
||||
queryText?: string,
|
||||
splitterDirection?: "horizontal" | "vertical",
|
||||
queryViewSizePercent?: number,
|
||||
) {
|
||||
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
||||
const collection: ViewModels.Collection = source.collection || source;
|
||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||
|
||||
@@ -695,9 +681,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
node: this,
|
||||
partitionKey: collection.partitionKey,
|
||||
onLoadStartKey: startKey,
|
||||
queryText,
|
||||
splitterDirection,
|
||||
queryViewSizePercent,
|
||||
},
|
||||
{
|
||||
container: this.container,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { userContext } from "UserContext";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import * as React from "react";
|
||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||
import ErrorImage from "../../../../images/error.svg";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { HttpHeaders } from "../../../Common/Constants";
|
||||
import { BackendApi, HttpHeaders } from "../../../Common/Constants";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import { GenerateTokenResponse } from "../../../Contracts/DataModels";
|
||||
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
|
||||
|
||||
interface Props {
|
||||
@@ -17,6 +19,10 @@ interface Props {
|
||||
}
|
||||
|
||||
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) {
|
||||
return await fetchEncryptedToken_ToBeDeprecated(connectionString);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken";
|
||||
@@ -29,10 +35,28 @@ export const fetchEncryptedToken = async (connectionString: string): Promise<str
|
||||
return decodeURIComponent(encryptedTokenResponse);
|
||||
};
|
||||
|
||||
export const fetchEncryptedToken_ToBeDeprecated = async (connectionString: string): Promise<string> => {
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
||||
const response = await fetch(url, { headers, method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
}
|
||||
// This API has a quirk where it must be parsed twice
|
||||
const result: GenerateTokenResponse = JSON.parse(await response.json());
|
||||
return decodeURIComponent(result.readWrite || result.read);
|
||||
};
|
||||
|
||||
export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise<boolean> => {
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
|
||||
|
||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.AccountRestrictions)
|
||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
|
||||
const url = backendEndpoint + "/api/guest/accountrestrictions/checkconnectionstringlogin";
|
||||
const response = await fetch(url, { headers, method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
|
||||
@@ -16,7 +16,6 @@ export type Features = {
|
||||
readonly enableAadDataPlane: boolean;
|
||||
readonly enableResourceGraph: boolean;
|
||||
readonly enableKoResourceTree: boolean;
|
||||
readonly enableThroughputBuckets: boolean;
|
||||
readonly hostedDataExplorer: boolean;
|
||||
readonly junoEndpoint?: string;
|
||||
readonly phoenixEndpoint?: string;
|
||||
@@ -82,7 +81,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
enableSpark: "true" === get("enablespark"),
|
||||
enableTtl: "true" === get("enablettl"),
|
||||
enableKoResourceTree: "true" === get("enablekoresourcetree"),
|
||||
enableThroughputBuckets: "true" === get("enablethroughputbuckets"),
|
||||
executeSproc: "true" === get("dataexplorerexecutesproc"),
|
||||
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
|
||||
mongoProxyEndpoint: get("mongoproxyendpoint"),
|
||||
|
||||
@@ -266,10 +266,7 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
||||
method: "GET",
|
||||
apiVersion: "2023-05-01-preview",
|
||||
queryParams: {
|
||||
filter:
|
||||
"armRegionName eq '" +
|
||||
regionShortName +
|
||||
"' and productDisplayName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
|
||||
filter: "armRegionName eq '" + regionShortName + "'",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { userContext } from "UserContext";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
// The component name whose state is being saved. Component name must not include special characters.
|
||||
export enum AppStateComponentNames {
|
||||
DocumentsTab = "DocumentsTab",
|
||||
MostRecentActivity = "MostRecentActivity",
|
||||
QueryCopilot = "QueryCopilot",
|
||||
DataExplorerAction = "DataExplorerAction",
|
||||
}
|
||||
|
||||
// Subcomponent for DataExplorerAction
|
||||
export const OPEN_TABS_SUBCOMPONENT_NAME = "OpenTabs";
|
||||
|
||||
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
||||
const SCHEMA_VERSION = 1;
|
||||
|
||||
@@ -80,18 +72,12 @@ export const hasState = (path: StorePath): boolean => {
|
||||
};
|
||||
|
||||
// This is for high-frequency state changes
|
||||
// Keep track of timeouts per path
|
||||
const pathToTimeoutIdMap = new Map<string, NodeJS.Timeout>();
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
|
||||
const key = createKeyFromPath(path);
|
||||
const timeoutId = pathToTimeoutIdMap.get(key);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
pathToTimeoutIdMap.set(
|
||||
key,
|
||||
setTimeout(() => saveState(path, state), debounceDelayMs),
|
||||
);
|
||||
timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs);
|
||||
};
|
||||
|
||||
interface ApplicationState {
|
||||
@@ -126,93 +112,3 @@ export const createKeyFromPath = (path: StorePath): string => {
|
||||
export const deleteAllStates = (): void => {
|
||||
LocalStorageUtility.removeEntry(StorageKey.AppState);
|
||||
};
|
||||
|
||||
// Convenience functions
|
||||
|
||||
/**
|
||||
*
|
||||
* @param subComponentName
|
||||
* @param collection
|
||||
* @param defaultValue Will be returned if persisted state is not found
|
||||
* @returns
|
||||
*/
|
||||
export const readSubComponentState = <T>(
|
||||
componentName: AppStateComponentNames,
|
||||
subComponentName: string,
|
||||
collection: ViewModels.CollectionBase | undefined,
|
||||
defaultValue: T,
|
||||
): T => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const state = loadState({
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection ? collection.databaseId : "",
|
||||
containerName: collection ? collection.id() : "",
|
||||
}) as T;
|
||||
|
||||
return state || defaultValue;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param subComponentName
|
||||
* @param collection
|
||||
* @param state State to save
|
||||
* @param debounce true for high-frequency calls (e.g mouse drag events)
|
||||
*/
|
||||
export const saveSubComponentState = <T>(
|
||||
componentName: AppStateComponentNames,
|
||||
subComponentName: string,
|
||||
collection: ViewModels.CollectionBase | undefined,
|
||||
state: T,
|
||||
debounce?: boolean,
|
||||
): void => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
|
||||
return;
|
||||
}
|
||||
|
||||
(debounce ? saveStateDebounced : saveState)(
|
||||
{
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection ? collection.databaseId : "",
|
||||
containerName: collection ? collection.id() : "",
|
||||
},
|
||||
state,
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteSubComponentState = (
|
||||
componentName: AppStateComponentNames,
|
||||
subComponentName: string,
|
||||
collection: ViewModels.CollectionBase,
|
||||
) => {
|
||||
const globalAccountName = userContext.databaseAccount?.name;
|
||||
if (!globalAccountName) {
|
||||
const message = "Database account name not found in userContext";
|
||||
console.error(message);
|
||||
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
|
||||
return;
|
||||
}
|
||||
|
||||
deleteState({
|
||||
componentName: componentName,
|
||||
subComponentName,
|
||||
globalAccountName,
|
||||
databaseName: collection.databaseId,
|
||||
containerName: collection.id(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ export interface UserContext {
|
||||
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
||||
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
||||
readonly dataPlaneRbacEnabled?: boolean;
|
||||
readonly refreshCosmosClient?: boolean;
|
||||
readonly hasDataPlaneRbacSettingChanged?: boolean;
|
||||
}
|
||||
|
||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import { configContext } from "ConfigContext";
|
||||
import * as Logger from "../Common/Logger";
|
||||
|
||||
export function validateEndpoint(
|
||||
@@ -66,6 +73,9 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
|
||||
//"https://main2.documentdb.ext.azure.com": ["104.42.196.69"],
|
||||
"https://main.documentdb.ext.azure.cn": ["139.217.8.252"],
|
||||
"https://main.documentdb.ext.azure.us": ["52.244.48.71"],
|
||||
// Add ussec and usnat when endpoint address is known:
|
||||
//ussec: ["29.26.26.67", "29.26.26.66"],
|
||||
//usnat: ["7.28.202.68"],
|
||||
};
|
||||
|
||||
export const PortalBackendOutboundIPs: { [key: string]: string[] } = {
|
||||
@@ -83,13 +93,21 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
||||
};
|
||||
|
||||
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
];
|
||||
|
||||
export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
"https://main.cosmos.ext.azure",
|
||||
"https://localhost:12901",
|
||||
];
|
||||
|
||||
export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
@@ -123,7 +141,9 @@ export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspac
|
||||
|
||||
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
|
||||
|
||||
export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = ["https://dataexplorer-preview.azurewebsites.net/"];
|
||||
export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = [
|
||||
"https://cosmos-explorer-preview.azurewebsites.net/",
|
||||
];
|
||||
|
||||
export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
JunoEndpoints.Test,
|
||||
@@ -135,3 +155,53 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
];
|
||||
|
||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
||||
|
||||
//
|
||||
// Temporary function to determine if a portal backend API is supported by the
|
||||
// new backend in this environment.
|
||||
//
|
||||
// TODO: Remove this function once new backend migration is completed for all environments.
|
||||
//
|
||||
export function useNewPortalBackendEndpoint(backendApi: string): boolean {
|
||||
// This maps backend APIs to the environments supported by the new backend.
|
||||
const newBackendApiEnvironmentMap: { [key: string]: string[] } = {
|
||||
[BackendApi.GenerateToken]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.PortalSettings]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.AccountRestrictions]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.RuntimeProxy]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.DisallowedLocations]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
PortalBackendEndpoints.Fairfax,
|
||||
PortalBackendEndpoints.Mooncake,
|
||||
],
|
||||
[BackendApi.SampleData]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
};
|
||||
|
||||
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT);
|
||||
}
|
||||
|
||||
104
src/Utils/NetworkUtility.test.ts
Normal file
104
src/Utils/NetworkUtility.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import { resetConfigContext, updateConfigContext } from "ConfigContext";
|
||||
import { DatabaseAccount, IpRule } from "Contracts/DataModels";
|
||||
import { updateUserContext } from "UserContext";
|
||||
import { MongoProxyOutboundIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
|
||||
import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
|
||||
|
||||
describe("NetworkUtility tests", () => {
|
||||
describe("getNetworkSettingsWarningMessage", () => {
|
||||
const publicAccessMessagePart = "Please enable public access to proceed";
|
||||
const accessMessagePart = "Please allow access from Azure Portal to proceed";
|
||||
let warningMessageResult: string;
|
||||
const warningMessageFunc = (msg: string) => (warningMessageResult = msg);
|
||||
|
||||
beforeEach(() => {
|
||||
warningMessageResult = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetConfigContext();
|
||||
});
|
||||
|
||||
it("should return no message when publicNetworkAccess is enabled", async () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
publicNetworkAccess: "Enabled",
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
await getNetworkSettingsWarningMessage(warningMessageFunc);
|
||||
expect(warningMessageResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return publicAccessMessage when publicNetworkAccess is disabled", async () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
publicNetworkAccess: "Disabled",
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
await getNetworkSettingsWarningMessage(warningMessageFunc);
|
||||
expect(warningMessageResult).toContain(publicAccessMessagePart);
|
||||
});
|
||||
|
||||
it(`should return no message when the appropriate ip rules are added to mongo/cassandra account per endpoint`, async () => {
|
||||
const portalBackendOutboundIPs: string[] = [
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
|
||||
...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac],
|
||||
...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod],
|
||||
];
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
kind: "MongoDB",
|
||||
properties: {
|
||||
ipRules: portalBackendOutboundIPs.map((ip: string) => ({ ipAddressOrRange: ip }) as IpRule),
|
||||
publicNetworkAccess: "Enabled",
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
updateConfigContext({
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
|
||||
});
|
||||
|
||||
let asyncWarningMessageResult: string;
|
||||
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
|
||||
|
||||
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
|
||||
expect(asyncWarningMessageResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return accessMessage when incorrent ip rule is added to mongo/cassandra account per endpoint", async () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
kind: "MongoDB",
|
||||
properties: {
|
||||
ipRules: [{ ipAddressOrRange: "1.1.1.1" }],
|
||||
publicNetworkAccess: "Enabled",
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
updateConfigContext({
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
|
||||
});
|
||||
|
||||
let asyncWarningMessageResult: string;
|
||||
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
|
||||
|
||||
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
|
||||
expect(asyncWarningMessageResult).toContain(accessMessagePart);
|
||||
});
|
||||
|
||||
// Postgres and vcore mongo account checks basically pass through to CheckFirewallRules so those
|
||||
// tests are omitted here and included in CheckFirewallRules.test.ts
|
||||
});
|
||||
});
|
||||
99
src/Utils/NetworkUtility.ts
Normal file
99
src/Utils/NetworkUtility.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { CassandraProxyEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||
import { userContext } from "UserContext";
|
||||
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
|
||||
|
||||
export const getNetworkSettingsWarningMessage = async (
|
||||
setStateFunc: (warningMessage: string) => void,
|
||||
): Promise<void> => {
|
||||
const accountProperties = userContext.databaseAccount?.properties;
|
||||
const accessMessage =
|
||||
"The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed.";
|
||||
const publicAccessMessage =
|
||||
"The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed.";
|
||||
|
||||
if (userContext.apiType === "Postgres") {
|
||||
checkFirewallRules(
|
||||
"2022-11-08",
|
||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
|
||||
undefined,
|
||||
setStateFunc,
|
||||
accessMessage,
|
||||
);
|
||||
return;
|
||||
} else if (userContext.apiType === "VCoreMongo") {
|
||||
checkFirewallRules(
|
||||
"2023-03-01-preview",
|
||||
(rule) =>
|
||||
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
||||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
|
||||
undefined,
|
||||
setStateFunc,
|
||||
accessMessage,
|
||||
);
|
||||
return;
|
||||
} else if (accountProperties) {
|
||||
// public network access is disabled
|
||||
if (
|
||||
accountProperties.publicNetworkAccess !== "Enabled" &&
|
||||
accountProperties.publicNetworkAccess !== "SecuredByPerimeter"
|
||||
) {
|
||||
setStateFunc(publicAccessMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const ipRules = accountProperties.ipRules;
|
||||
// public network access is NOT set to "All networks"
|
||||
if (ipRules?.length > 0) {
|
||||
const isProdOrMpacPortalBackendEndpoint: boolean = [
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
].includes(configContext.PORTAL_BACKEND_ENDPOINT);
|
||||
const portalBackendOutboundIPs: string[] = isProdOrMpacPortalBackendEndpoint
|
||||
? [
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
|
||||
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
|
||||
]
|
||||
: PortalBackendOutboundIPs[configContext.PORTAL_BACKEND_ENDPOINT];
|
||||
let portalIPs: string[] = [...portalBackendOutboundIPs];
|
||||
|
||||
if (userContext.apiType === "Mongo") {
|
||||
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
|
||||
configContext.MONGO_PROXY_ENDPOINT,
|
||||
);
|
||||
|
||||
const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint
|
||||
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
||||
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
||||
|
||||
portalIPs = [...portalIPs, ...mongoProxyOutboundIPs];
|
||||
} else if (userContext.apiType === "Cassandra") {
|
||||
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
].includes(configContext.CASSANDRA_PROXY_ENDPOINT);
|
||||
|
||||
const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint
|
||||
? [
|
||||
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac],
|
||||
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod],
|
||||
]
|
||||
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
||||
|
||||
portalIPs = [...portalIPs, ...cassandraProxyOutboundIPs];
|
||||
}
|
||||
|
||||
let numberOfMatches = 0;
|
||||
ipRules.forEach((ipRule) => {
|
||||
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {
|
||||
numberOfMatches++;
|
||||
}
|
||||
});
|
||||
|
||||
if (numberOfMatches !== portalIPs.length) {
|
||||
setStateFunc(accessMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */
|
||||
export async function listCassandraKeyspaces(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account and collection. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given collection, split by partition. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given collection and region, split by partition. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account, collection and region. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account and database. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account and region. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the properties of an existing Azure Cosmos DB database account. */
|
||||
export async function get(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the graphs under an existing Azure Cosmos DB database account. */
|
||||
export async function listGraphs(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the Gremlin databases under an existing Azure Cosmos DB database account. */
|
||||
export async function listGremlinDatabases(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* List Cosmos DB locations and their properties */
|
||||
export async function list(subscriptionId: string): Promise<Types.LocationListResult | Types.CloudError> {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the MongoDB databases under an existing Azure Cosmos DB database account. */
|
||||
export async function listMongoDBDatabases(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists all of the available Cosmos DB Resource Provider operations. */
|
||||
export async function list(): Promise<Types.OperationListResult> {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given partition key range id. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given partition key range id and region. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account. This url is only for PBS and Replication Latency data */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given account, source and target region. This url is only for PBS and Replication Latency data */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given account target region. This url is only for PBS and Replication Latency data */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-05-15-preview";
|
||||
|
||||
/* Lists the SQL databases under an existing Azure Cosmos DB database account. */
|
||||
export async function listSqlDatabases(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
|
||||
/* Lists the Tables under an existing Azure Cosmos DB database account. */
|
||||
export async function listTables(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
/* The List operation response, that contains the client encryption keys and their properties. */
|
||||
@@ -553,12 +553,6 @@ export interface DatabaseAccountGetProperties {
|
||||
/* The object that represents all properties related to capacity enforcement on an account. */
|
||||
capacity?: Capacity;
|
||||
|
||||
/* Indicates the capacityMode of the Cosmos DB account. */
|
||||
capacityMode?: CapacityMode;
|
||||
|
||||
/* The object that represents the migration state for the CapacityMode of the Cosmos DB account. */
|
||||
capacityModeChangeTransitionState?: CapacityModeChangeTransitionState;
|
||||
|
||||
/* Flag to indicate whether to enable MaterializedViews on the Cosmos DB account */
|
||||
enableMaterializedViews?: boolean;
|
||||
/* The object that represents the metadata for the Account Keys of the Cosmos DB account. */
|
||||
@@ -658,9 +652,6 @@ export interface DatabaseAccountCreateUpdateProperties {
|
||||
/* The object that represents all properties related to capacity enforcement on an account. */
|
||||
capacity?: Capacity;
|
||||
|
||||
/* Indicates the capacityMode of the Cosmos DB account. */
|
||||
capacityMode?: CapacityMode;
|
||||
|
||||
/* Flag to indicate whether to enable MaterializedViews on the Cosmos DB account */
|
||||
enableMaterializedViews?: boolean;
|
||||
/* This property is ignored during the update/create operation, as the metadata is read-only. The object represents the metadata for the Account Keys of the Cosmos DB account. */
|
||||
@@ -763,9 +754,6 @@ export interface DatabaseAccountUpdateProperties {
|
||||
/* The object that represents all properties related to capacity enforcement on an account. */
|
||||
capacity?: Capacity;
|
||||
|
||||
/* Indicates the capacityMode of the Cosmos DB account. */
|
||||
capacityMode?: CapacityMode;
|
||||
|
||||
/* Flag to indicate whether to enable MaterializedViews on the Cosmos DB account */
|
||||
enableMaterializedViews?: boolean;
|
||||
/* This property is ignored during the update operation, as the metadata is read-only. The object represents the metadata for the Account Keys of the Cosmos DB account. */
|
||||
@@ -1099,8 +1087,6 @@ export interface ThroughputSettingsResource {
|
||||
readonly instantMaximumThroughput?: string;
|
||||
/* The maximum throughput value or the maximum maxThroughput value (for autoscale) that can be specified */
|
||||
readonly softAllowedMaximumThroughput?: string;
|
||||
/* Array of Throughput Bucket limits to be applied to the Cosmos DB container */
|
||||
throughputBuckets?: ThroughputBucketResource[];
|
||||
}
|
||||
|
||||
/* Cosmos DB provisioned throughput settings object */
|
||||
@@ -1128,14 +1114,6 @@ export interface ThroughputPolicyResource {
|
||||
incrementPercent?: number;
|
||||
}
|
||||
|
||||
/* Cosmos DB throughput bucket object */
|
||||
export interface ThroughputBucketResource {
|
||||
/* Represents the throughput bucket id */
|
||||
id: number;
|
||||
/* Represents maximum percentage throughput that can be used by the bucket */
|
||||
maxThroughputPercentage: number;
|
||||
}
|
||||
|
||||
/* Cosmos DB options resource object */
|
||||
export interface OptionsResource {
|
||||
/* Value of the Cosmos DB resource throughput or autoscaleSettings. Use the ThroughputSetting resource when retrieving offer details. */
|
||||
@@ -1257,6 +1235,10 @@ export interface SqlDatabaseResource {
|
||||
export interface SqlContainerResource {
|
||||
/* Name of the Cosmos DB SQL container */
|
||||
id: string;
|
||||
|
||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||
fullTextPolicy?: FullTextPolicy;
|
||||
|
||||
/* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
|
||||
@@ -1287,11 +1269,39 @@ export interface SqlContainerResource {
|
||||
|
||||
/* List of computed properties */
|
||||
computedProperties?: ComputedProperty[];
|
||||
}
|
||||
|
||||
/* The vector embedding policy for the container. */
|
||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||
export interface VectorEmbeddingPolicy {
|
||||
vectorEmbeddings: VectorEmbedding[];
|
||||
}
|
||||
|
||||
fullTextPolicy?: FullTextPolicy;
|
||||
export interface VectorEmbedding {
|
||||
path?: string;
|
||||
dataType?: string;
|
||||
dimensions?: number;
|
||||
distanceFunction?: string;
|
||||
}
|
||||
|
||||
export interface FullTextPolicy {
|
||||
/**
|
||||
* The default language for the full text .
|
||||
*/
|
||||
defaultLanguage: string;
|
||||
/**
|
||||
* The paths to be indexed for full text search.
|
||||
*/
|
||||
fullTextPaths: FullTextPath[];
|
||||
}
|
||||
|
||||
export interface FullTextPath {
|
||||
/**
|
||||
* The path to be indexed for full text search.
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* The language for the full text path.
|
||||
*/
|
||||
language: string;
|
||||
}
|
||||
|
||||
/* Cosmos DB indexing policy */
|
||||
@@ -1313,14 +1323,19 @@ export interface IndexingPolicy {
|
||||
/* List of spatial specifics */
|
||||
spatialIndexes?: SpatialSpec[];
|
||||
|
||||
/* List of paths to include in the vector indexing */
|
||||
vectorIndexes?: VectorIndex[];
|
||||
|
||||
fullTextIndexes?: FullTextIndex[];
|
||||
}
|
||||
|
||||
/* Cosmos DB Vector Embedding Policy */
|
||||
export interface VectorEmbeddingPolicy {
|
||||
/* List of vector embeddings */
|
||||
vectorEmbeddings?: VectorEmbedding[];
|
||||
export interface VectorIndex {
|
||||
path?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface FullTextIndex {
|
||||
/** The path in the JSON document to index. */
|
||||
path: string;
|
||||
}
|
||||
|
||||
/* undocumented */
|
||||
@@ -1348,50 +1363,6 @@ export interface Indexes {
|
||||
kind?: "Hash" | "Range" | "Spatial";
|
||||
}
|
||||
|
||||
/* undocumented */
|
||||
export interface VectorIndex {
|
||||
/* The path to the vector field in the document. */
|
||||
path: string;
|
||||
/* The index type of the vector. Currently, flat, diskANN, and quantizedFlat are supported. */
|
||||
type: "flat" | "diskANN" | "quantizedFlat";
|
||||
}
|
||||
|
||||
/* Represents a vector embedding. A vector embedding is used to define a vector field in the documents. */
|
||||
export interface VectorEmbedding {
|
||||
/* The path to the vector field in the document. */
|
||||
path: string;
|
||||
/* Indicates the data type of vector. */
|
||||
dataType: "float16" | "float32" | "uint8" | "int8";
|
||||
|
||||
/* The distance function to use for distance calculation in between vectors. */
|
||||
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
||||
|
||||
/* The number of dimensions in the vector. */
|
||||
dimensions: number;
|
||||
}
|
||||
|
||||
export interface FullTextPolicy {
|
||||
/**
|
||||
* The default language for the full text .
|
||||
*/
|
||||
defaultLanguage: string;
|
||||
/**
|
||||
* The paths to be indexed for full text search.
|
||||
*/
|
||||
fullTextPaths: FullTextPath[];
|
||||
}
|
||||
|
||||
export interface FullTextPath {
|
||||
/**
|
||||
* The path to be indexed for full text search.
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* The language for the full text path.
|
||||
*/
|
||||
language: string;
|
||||
}
|
||||
|
||||
/* List of composite path */
|
||||
export type CompositePathList = CompositePath[];
|
||||
|
||||
@@ -1717,28 +1688,6 @@ export interface Capacity {
|
||||
totalThroughputLimit?: number;
|
||||
}
|
||||
|
||||
/* Indicates the capacity mode of the account. */
|
||||
export type CapacityMode = "None" | "Provisioned" | "Serverless";
|
||||
|
||||
/* The transition state information related capacity mode change with update request. */
|
||||
export interface CapacityModeChangeTransitionState {
|
||||
/* The transition status of capacity mode. */
|
||||
capacityModeTransitionStatus?: "Invalid" | "Initialized" | "InProgress" | "Completed" | "Failed";
|
||||
|
||||
/* Indicates the current capacity mode of the account. */
|
||||
currentCapacityMode?: "None" | "Provisioned" | "Serverless";
|
||||
|
||||
/* Indicates the previous capacity mode of the account before successful transition. */
|
||||
previousCapacityMode?: "None" | "Provisioned" | "Serverless";
|
||||
|
||||
/* Begin time in UTC of the capacity mode change. */
|
||||
readonly capacityModeTransitionBeginTimestamp?: string;
|
||||
/* End time in UTC of the capacity mode change. */
|
||||
readonly capacityModeTransitionEndTimestamp?: string;
|
||||
/* End time in UTC of the last successful capacity mode change. */
|
||||
readonly capacityModeLastSuccessfulTransitionEndTimestamp?: string;
|
||||
}
|
||||
|
||||
/* Tags are a list of key-value pairs that describe the resource. These tags can be used in viewing and grouping this resource (across resource groups). A maximum of 15 tags can be provided for a resource. Each tag must have a key no greater than 128 characters and value no greater than 256 characters. For example, the default experience for a template type is set with "defaultExperience": "Cassandra". Current "defaultExperience" values also include "Table", "Graph", "DocumentDB", and "MongoDB". */
|
||||
export type Tags = { [key: string]: string };
|
||||
|
||||
@@ -2004,8 +1953,8 @@ export type PublicNetworkAccess = "Enabled" | "Disabled" | "SecuredByPerimeter";
|
||||
|
||||
/* undocumented */
|
||||
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";
|
||||
/* Describes the ServerVersion of an a MongoDB account. */
|
||||
serverVersion?: "3.2" | "3.6" | "4.0" | "4.2";
|
||||
}
|
||||
|
||||
/* Analytical storage specific properties. */
|
||||
|
||||
@@ -7,12 +7,9 @@ import Explorer from "Explorer/Explorer";
|
||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||
import {
|
||||
AppStateComponentNames,
|
||||
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||
readSubComponentState,
|
||||
} from "Shared/AppStatePersistenceUtility";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
@@ -83,9 +80,6 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||
await updateContextForCopilot(explorer);
|
||||
await updateContextForSampleData(explorer);
|
||||
}
|
||||
|
||||
restoreOpenTabs();
|
||||
|
||||
setExplorer(explorer);
|
||||
}
|
||||
};
|
||||
@@ -95,6 +89,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||
useEffect(() => {
|
||||
if (explorer) {
|
||||
applyExplorerBindings(explorer);
|
||||
explorer.openNPSSurveyDialog();
|
||||
}
|
||||
}, [explorer]);
|
||||
|
||||
@@ -137,7 +132,7 @@ async function configureFabric(): Promise<Explorer> {
|
||||
await scheduleRefreshDatabaseResourceToken(true);
|
||||
resolve(explorer);
|
||||
await explorer.refreshAllDatabases();
|
||||
if (userContext.fabricContext.isVisible) {
|
||||
if (userContext.fabricContext.isVisible && !firstContainerOpened) {
|
||||
firstContainerOpened = true;
|
||||
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
|
||||
}
|
||||
@@ -180,11 +175,6 @@ async function configureFabric(): Promise<Explorer> {
|
||||
}
|
||||
|
||||
const openFirstContainer = async (explorer: Explorer, databaseName: string, collectionName?: string) => {
|
||||
if (useTabs.getState().openedTabs.length > 0) {
|
||||
// Don't open any tabs if there are already tabs open
|
||||
return;
|
||||
}
|
||||
|
||||
// Expand database first
|
||||
databaseName = sessionStorage.getItem("openDatabaseName") ?? databaseName;
|
||||
const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
|
||||
@@ -439,7 +429,6 @@ function createExplorerFabric(params: { connectionId: string; isVisible: boolean
|
||||
},
|
||||
},
|
||||
});
|
||||
useTabs.getState().closeAllTabs();
|
||||
const explorer = new Explorer();
|
||||
return explorer;
|
||||
}
|
||||
@@ -547,6 +536,14 @@ async function configurePortal(): Promise<Explorer> {
|
||||
const inputs = message?.inputs;
|
||||
const openAction = message?.openAction;
|
||||
if (inputs) {
|
||||
if (
|
||||
configContext.BACKEND_ENDPOINT &&
|
||||
configContext.platform === Platform.Portal &&
|
||||
process.env.NODE_ENV === "development"
|
||||
) {
|
||||
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||
}
|
||||
|
||||
updateContextsFromPortalMessage(inputs);
|
||||
|
||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||
@@ -671,17 +668,18 @@ function updateAADEndpoints(portalEnv: PortalEnv) {
|
||||
|
||||
function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
if (
|
||||
configContext.PORTAL_BACKEND_ENDPOINT &&
|
||||
configContext.BACKEND_ENDPOINT &&
|
||||
configContext.platform === Platform.Portal &&
|
||||
process.env.NODE_ENV === "development"
|
||||
) {
|
||||
inputs.portalBackendEndpoint = configContext.PROXY_PATH;
|
||||
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||
}
|
||||
|
||||
const authorizationToken = inputs.authorizationToken || "";
|
||||
const databaseAccount = inputs.databaseAccount;
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
|
||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||
MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint,
|
||||
CASSANDRA_PROXY_ENDPOINT: inputs.cassandraProxyEndpoint,
|
||||
@@ -733,6 +731,8 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
}
|
||||
}
|
||||
|
||||
getNetworkSettingsWarningMessage(useTabs.getState().setNetworkSettingsWarning);
|
||||
|
||||
if (inputs.features) {
|
||||
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
||||
}
|
||||
@@ -784,7 +784,17 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const url: string = createUri(configContext.PORTAL_BACKEND_ENDPOINT, "/api/sampledata");
|
||||
let url: string;
|
||||
if (useNewPortalBackendEndpoint(Constants.BackendApi.SampleData)) {
|
||||
url = createUri(configContext.PORTAL_BACKEND_ENDPOINT, "/api/sampledata");
|
||||
} else {
|
||||
const sampleDatabaseEndpoint = useQueryCopilot.getState().copilotUserDBEnabled
|
||||
? `/api/tokens/sampledataconnection/v2`
|
||||
: `/api/tokens/sampledataconnection`;
|
||||
|
||||
url = createUri(`${configContext.BACKEND_ENDPOINT}`, sampleDatabaseEndpoint);
|
||||
}
|
||||
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||
|
||||
@@ -800,23 +810,9 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
||||
const sampleDataConnectionInfo = parseResourceTokenConnectionString(data.connectionString);
|
||||
updateUserContext({ sampleDataConnectionInfo });
|
||||
|
||||
explorer.refreshSampleData();
|
||||
await explorer.refreshSampleData();
|
||||
}
|
||||
|
||||
interface SampledataconnectionResponse {
|
||||
connectionString: string;
|
||||
}
|
||||
|
||||
const restoreOpenTabs = () => {
|
||||
const openTabsState = readSubComponentState<(DataExplorerAction | undefined)[]>(
|
||||
AppStateComponentNames.DataExplorerAction,
|
||||
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||
undefined,
|
||||
[],
|
||||
);
|
||||
openTabsState.forEach((openTabState) => {
|
||||
if (openTabState) {
|
||||
handleOpenAction(openTabState, useDatabases.getState().databases, this);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { HttpHeaders } from "../Common/Constants";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { ApiEndpoints, BackendApi, HttpHeaders } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { AccessInputMetadata } from "../Contracts/DataModels";
|
||||
|
||||
const url = `${configContext.BACKEND_ENDPOINT}${ApiEndpoints.guestRuntimeProxy}/accessinputmetadata?_=1609359229955`;
|
||||
|
||||
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
||||
return fetchAccessData_ToBeDeprecated(portalToken);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
// Portal encrypted token API quirk: The token header must be URL encoded
|
||||
headers.append(HttpHeaders.guestAccessToken, encodeURIComponent(portalToken));
|
||||
@@ -18,6 +25,25 @@ export async function fetchAccessData(portalToken: string): Promise<AccessInputM
|
||||
.catch((error) => console.error(error));
|
||||
}
|
||||
|
||||
export async function fetchAccessData_ToBeDeprecated(portalToken: string): Promise<AccessInputMetadata> {
|
||||
const headers = new Headers();
|
||||
// Portal encrypted token API quirk: The token header must be URL encoded
|
||||
headers.append(HttpHeaders.guestAccessToken, encodeURIComponent(portalToken));
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: headers,
|
||||
};
|
||||
|
||||
return (
|
||||
fetch(url, options)
|
||||
.then((response) => response.json())
|
||||
// Portal encrypted token API quirk: The response is double JSON encoded
|
||||
.then((json) => JSON.parse(json))
|
||||
.catch((error) => console.error(error))
|
||||
);
|
||||
}
|
||||
|
||||
export function useTokenMetadata(token: string): AccessInputMetadata | undefined {
|
||||
const [state, setState] = useState<AccessInputMetadata | undefined>();
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { clamp } from "@fluentui/react";
|
||||
import { OpenTab } from "Contracts/ActionContracts";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import {
|
||||
AppStateComponentNames,
|
||||
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||
saveSubComponentState,
|
||||
} from "Shared/AppStatePersistenceUtility";
|
||||
import create, { UseStore } from "zustand";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { CollectionTabKind } from "../Contracts/ViewModels";
|
||||
@@ -18,6 +12,7 @@ export interface TabsState {
|
||||
openedReactTabs: ReactTabKind[];
|
||||
activeTab: TabsBase | undefined;
|
||||
activeReactTab: ReactTabKind | undefined;
|
||||
networkSettingsWarning: string;
|
||||
queryCopilotTabInitialInput: string;
|
||||
isTabExecuting: boolean;
|
||||
isQueryErrorThrown: boolean;
|
||||
@@ -32,6 +27,7 @@ export interface TabsState {
|
||||
closeAllNotebookTabs: (hardClose: boolean) => void;
|
||||
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
||||
closeReactTab: (tabKind: ReactTabKind) => void;
|
||||
setNetworkSettingsWarning: (warningMessage: string) => void;
|
||||
setQueryCopilotTabInitialInput: (input: string) => void;
|
||||
setIsTabExecuting: (state: boolean) => void;
|
||||
setIsQueryErrorThrown: (state: boolean) => void;
|
||||
@@ -40,8 +36,6 @@ export interface TabsState {
|
||||
selectLeftTab: () => void;
|
||||
selectRightTab: () => void;
|
||||
closeActiveTab: () => void;
|
||||
closeAllTabs: () => void;
|
||||
persistTabsState: () => void;
|
||||
}
|
||||
|
||||
export enum ReactTabKind {
|
||||
@@ -67,6 +61,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [],
|
||||
activeTab: undefined,
|
||||
activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined,
|
||||
networkSettingsWarning: "",
|
||||
queryCopilotTabInitialInput: "",
|
||||
isTabExecuting: false,
|
||||
isQueryErrorThrown: false,
|
||||
@@ -78,9 +73,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
},
|
||||
activateNewTab: (tab: TabsBase): void => {
|
||||
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
|
||||
tab.triggerPersistState = get().persistTabsState;
|
||||
tab.onActivate();
|
||||
get().persistTabsState();
|
||||
},
|
||||
activateReactTab: (tabKind: ReactTabKind): void => {
|
||||
// Clear the selected node when switching to a react tab.
|
||||
@@ -137,8 +130,6 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
}
|
||||
|
||||
set({ openedTabs: updatedTabs });
|
||||
|
||||
get().persistTabsState();
|
||||
},
|
||||
closeAllNotebookTabs: (hardClose): void => {
|
||||
const isNotebook = (tabKind: CollectionTabKind): boolean => {
|
||||
@@ -187,6 +178,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
|
||||
set({ openedReactTabs: updatedOpenedReactTabs });
|
||||
},
|
||||
setNetworkSettingsWarning: (warningMessage: string) => set({ networkSettingsWarning: warningMessage }),
|
||||
setQueryCopilotTabInitialInput: (input: string) => set({ queryCopilotTabInitialInput: input }),
|
||||
setIsTabExecuting: (state: boolean) => {
|
||||
set({ isTabExecuting: state });
|
||||
@@ -234,18 +226,4 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
state.closeTab(state.activeTab);
|
||||
}
|
||||
},
|
||||
closeAllTabs: () => {
|
||||
set({ openedTabs: [], openedReactTabs: [], activeTab: undefined, activeReactTab: undefined });
|
||||
},
|
||||
persistTabsState: () => {
|
||||
const state = get();
|
||||
const openTabsStates = state.openedTabs.map((tab) => tab.getPersistedState());
|
||||
|
||||
saveSubComponentState<OpenTab[]>(
|
||||
AppStateComponentNames.DataExplorerAction,
|
||||
OPEN_TABS_SUBCOMPONENT_NAME,
|
||||
undefined,
|
||||
openTabsStates,
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
10
test/fx.ts
10
test/fx.ts
@@ -1,4 +1,4 @@
|
||||
import { AzureCliCredential } from "@azure/identity";
|
||||
import { AzureCliCredentials } from "@azure/ms-rest-nodeauth";
|
||||
import { expect, Frame, Locator, Page } from "@playwright/test";
|
||||
import crypto from "crypto";
|
||||
|
||||
@@ -20,13 +20,13 @@ export function generateUniqueName(baseName, options?: TestNameOptions): string
|
||||
return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`;
|
||||
}
|
||||
|
||||
export function getAzureCLICredentials(): AzureCliCredential {
|
||||
return new AzureCliCredential();
|
||||
export async function getAzureCLICredentials(): Promise<AzureCliCredentials> {
|
||||
return await AzureCliCredentials.create();
|
||||
}
|
||||
|
||||
export async function getAzureCLICredentialsToken(): Promise<string> {
|
||||
const credentials = getAzureCLICredentials();
|
||||
const token = (await credentials.getToken("https://management.core.windows.net//.default")).token;
|
||||
const credentials = await getAzureCLICredentials();
|
||||
const token = (await credentials.getToken()).accessToken;
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "../fx";
|
||||
|
||||
test("SQL account using Resource token", async ({ page }) => {
|
||||
const credentials = getAzureCLICredentials();
|
||||
const credentials = await getAzureCLICredentials();
|
||||
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const accountName = getAccountName(TestAccount.SQL);
|
||||
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
||||
|
||||
@@ -56,7 +56,7 @@ export class TestContainerContext {
|
||||
export async function createTestSQLContainer(includeTestData?: boolean) {
|
||||
const databaseId = generateUniqueName("db");
|
||||
const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique
|
||||
const credentials = getAzureCLICredentials();
|
||||
const credentials = await getAzureCLICredentials();
|
||||
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const accountName = getAccountName(TestAccount.SQL);
|
||||
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
||||
|
||||
@@ -39,6 +39,7 @@ const initTestExplorer = async (): Promise<void> => {
|
||||
csmEndpoint: "https://management.azure.com",
|
||||
dnsSuffix: "documents.azure.com",
|
||||
serverId: "prod1",
|
||||
extensionEndpoint: "/proxy",
|
||||
portalBackendEndpoint: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
mongoProxyEndpoint: "https://cdb-ms-mpac-mp.cosmos.azure.com",
|
||||
cassandraProxyEndpoint: "https://cdb-ms-mpac-cp.cosmos.azure.com",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user