Compare commits

..

2 Commits

Author SHA1 Message Date
Sung-Hyun Kang
3ae92f2398 testing icons 2025-01-14 17:16:02 -06:00
Sung-Hyun Kang
a0853c9167 testing 2025-01-14 14:45:19 -06:00
188 changed files with 3720 additions and 42689 deletions

View File

@@ -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)

View File

@@ -83,7 +83,7 @@ jobs:
- run: npm ci - run: npm ci
- run: npm run build:contracts - run: npm run build:contracts
- name: Restore Build Cache - name: Restore Build Cache
uses: actions/cache@v4 uses: actions/cache@v2
with: with:
path: .cache path: .cache
key: ${{ runner.os }}-build-cache key: ${{ runner.os }}-build-cache
@@ -96,16 +96,14 @@ jobs:
with: with:
name: dist name: dist
path: dist/ path: dist/
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
- name: Upload build to preview blob storage - name: Upload build to preview blob storage
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --auth-mode login --overwrite true run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
- name: Upload preview config to blob storage - name: Upload preview config to blob storage
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --auth-mode login --overwrite true run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
@@ -115,21 +113,21 @@ jobs:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps: steps:
- uses: nuget/setup-nuget@v2
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder - name: Download Dist Folder
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: dist name: dist
- run: cp ./configs/prod.json config.json - run: cp ./configs/prod.json config.json
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}" - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE" - run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- run: dotnet nuget remove source "ADO"
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: Upload package to Artifacts name: packages
with: with:
name: prod-package path: "*.nupkg"
path: "bin/Release/*.nupkg"
nugetmpac: nugetmpac:
name: Publish Nuget MPAC name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
@@ -139,21 +137,22 @@ jobs:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps: steps:
- uses: nuget/setup-nuget@v2
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder - name: Download Dist Folder
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: dist name: dist
- run: cp ./configs/mpac.json config.json - run: cp ./configs/mpac.json config.json
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec - run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}" - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE" - run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- run: dotnet nuget remove source "ADO"
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: Upload package to Artifacts name: packages
with: with:
name: mpac-package path: "*.nupkg"
path: "bin/Release/*.nupkg"
playwright-tests: playwright-tests:
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})" name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"
@@ -164,8 +163,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [10] shardTotal: [8]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Az CLI login" - name: "Az CLI login"

View File

@@ -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>

View File

@@ -61,8 +61,6 @@
@GalleryBackgroundColor: #fdfdfd; @GalleryBackgroundColor: #fdfdfd;
@LinkColor: #2d6da4;
//Icons //Icons
@InfoIconColor: #0072c6; @InfoIconColor: #0072c6;
@WarningIconColor: #db7500; @WarningIconColor: #db7500;
@@ -248,10 +246,6 @@
outline: 1px dashed @FocusColor; outline: 1px dashed @FocusColor;
} }
.focusedBorder() {
border: 1px dashed @FocusColor;
}
/************************************************************************************************ /************************************************************************************************
Common Toggle Switch Common Toggle Switch
*************************************************************************************************/ *************************************************************************************************/

View File

@@ -1830,14 +1830,6 @@ input::-webkit-calendar-picker-indicator::after {
transform: rotate(90deg); transform: rotate(90deg);
} }
.customAccordion button:focus {
.focus();
}
.customAccordion {
margin-top: 1px;
}
.datalist-arrow:after:hover { .datalist-arrow:after:hover {
content: "\276F"; content: "\276F";
position: absolute; position: absolute;
@@ -1914,20 +1906,13 @@ input::-webkit-calendar-picker-indicator::after {
} }
.nav-tabs-margin { .nav-tabs-margin {
height: 32px;
background-color: #f2f2f2; background-color: #f2f2f2;
.nav-tabs { .nav-tabs {
display: flex; display: flex;
flex-wrap: wrap;
align-items: flex-end; align-items: flex-end;
height: 100%; height: 100%;
margin-bottom: -0.5px;
li {
// Override the bootstrap defaults here to align with our layout constants.
margin-bottom: 0px;
height: 32px;
}
} }
} }

567
package-lock.json generated
View File

@@ -12,7 +12,8 @@
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.2.0-beta.1", "@azure/cosmos": "4.2.0-beta.1",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0", "@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.1.1",
"@azure/msal-browser": "2.14.2", "@azure/msal-browser": "2.14.2",
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12", "@babel/plugin-proposal-decorators": "7.12.12",
@@ -86,7 +87,7 @@
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.44.0", "monaco-editor": "0.44.0",
"ms": "2.1.3", "ms": "2.1.3",
"p-retry": "6.2.1", "p-retry": "4.6.2",
"patch-package": "8.0.0", "patch-package": "8.0.0",
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42", "post-robot": "10.0.42",
@@ -427,68 +428,47 @@
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/@azure/identity": { "node_modules/@azure/identity": {
"version": "4.5.0", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.5.0.tgz", "license": "MIT",
"integrity": "sha512-EknvVmtBuSIic47xkOqyNabAme0RYTw52BTMz8eBgU1ysTyMrD1uOoM+JdS0J/4Yfp98IBT3osqq3BfwSaNaGQ==",
"dependencies": { "dependencies": {
"@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0",
"@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.0.0",
"@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.1.0",
"@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "1.0.0-preview.12",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0", "@azure/logger": "^1.0.0",
"@azure/msal-browser": "^3.26.1", "@azure/msal-node": "1.0.0-beta.6",
"@azure/msal-node": "^2.15.0", "@types/stoppable": "^1.1.0",
"axios": "^0.21.1",
"events": "^3.0.0", "events": "^3.0.0",
"jws": "^4.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", "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" "tslib": "^2.2.0"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=12.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_modules/@azure/identity/node_modules/tslib": { "node_modules/@azure/identity/node_modules/tslib": {
"version": "2.8.1", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "license": "0BSD"
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
}, },
"node_modules/@azure/logger": { "node_modules/@azure/logger": {
"version": "1.0.4", "version": "1.0.4",
@@ -504,6 +484,10 @@
"version": "2.6.2", "version": "2.6.2",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/@azure/ms-rest-azure-env": {
"version": "2.0.0",
"license": "MIT"
},
"node_modules/@azure/ms-rest-azure-js": { "node_modules/@azure/ms-rest-azure-js": {
"version": "2.1.0", "version": "2.1.0",
"license": "MIT", "license": "MIT",
@@ -575,6 +559,15 @@
"node": ">=4.0" "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": { "node_modules/@azure/msal-browser": {
"version": "2.14.2", "version": "2.14.2",
"license": "MIT", "license": "MIT",
@@ -596,24 +589,13 @@
} }
}, },
"node_modules/@azure/msal-node": { "node_modules/@azure/msal-node": {
"version": "2.16.2", "version": "1.0.0-beta.6",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz", "license": "MIT",
"integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==",
"dependencies": { "dependencies": {
"@azure/msal-common": "14.16.0", "@azure/msal-common": "^4.0.0",
"jsonwebtoken": "^9.0.0", "axios": "^0.21.1",
"jsonwebtoken": "^8.5.1",
"uuid": "^8.3.0" "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": { "node_modules/@babel/code-frame": {
@@ -10091,6 +10073,13 @@
"@octokit/openapi-types": "^19.0.2" "@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": { "node_modules/@phosphor/algorithm": {
"version": "1.2.0", "version": "1.2.0",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
@@ -12662,9 +12651,7 @@
} }
}, },
"node_modules/@types/retry": { "node_modules/@types/retry": {
"version": "0.12.2", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/sanitize-html": { "node_modules/@types/sanitize-html": {
@@ -12738,6 +12725,13 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/stoppable": {
"version": "1.1.3",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/styled-components": { "node_modules/@types/styled-components": {
"version": "5.1.1", "version": "5.1.1",
"dev": true, "dev": true,
@@ -13340,6 +13334,61 @@
"node": ">=0.4.0" "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": { "node_modules/address": {
"version": "1.1.2", "version": "1.1.2",
"dev": true, "dev": true,
@@ -13972,6 +14021,13 @@
"version": "1.12.0", "version": "1.12.0",
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": {
"version": "0.21.4",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/babel-core": { "node_modules/babel-core": {
"version": "7.0.0-bridge.0", "version": "7.0.0-bridge.0",
"dev": true, "dev": true,
@@ -14675,7 +14731,7 @@
}, },
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"dev": true, "devOptional": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -14747,9 +14803,8 @@
}, },
"node_modules/bl": { "node_modules/bl": {
"version": "4.1.0", "version": "4.1.0",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"buffer": "^5.5.0", "buffer": "^5.5.0",
"inherits": "^2.0.4", "inherits": "^2.0.4",
@@ -14758,7 +14813,7 @@
}, },
"node_modules/bl/node_modules/buffer": { "node_modules/bl/node_modules/buffer": {
"version": "5.7.1", "version": "5.7.1",
"dev": true, "devOptional": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -14774,7 +14829,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"base64-js": "^1.3.1", "base64-js": "^1.3.1",
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
@@ -14782,9 +14836,8 @@
}, },
"node_modules/bl/node_modules/readable-stream": { "node_modules/bl/node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"string_decoder": "^1.1.1", "string_decoder": "^1.1.1",
@@ -15354,6 +15407,11 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/chownr": {
"version": "1.1.4",
"license": "ISC",
"optional": true
},
"node_modules/chrome-trace-event": { "node_modules/chrome-trace-event": {
"version": "1.0.3", "version": "1.0.3",
"license": "MIT", "license": "MIT",
@@ -17117,6 +17175,13 @@
"version": "1.29.0", "version": "1.29.0",
"license": "MIT" "license": "MIT"
}, },
"node_modules/date-utils": {
"version": "1.2.21",
"license": "MIT",
"engines": {
"node": ">0.4.0"
}
},
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.8.19", "version": "1.8.19",
"license": "MIT" "license": "MIT"
@@ -17211,6 +17276,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"license": "MIT" "license": "MIT"
@@ -17271,6 +17344,7 @@
}, },
"node_modules/define-lazy-prop": { "node_modules/define-lazy-prop": {
"version": "2.0.0", "version": "2.0.0",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -18902,6 +18976,14 @@
"version": "2.0.0", "version": "2.0.0",
"license": "MIT" "license": "MIT"
}, },
"node_modules/expand-template": {
"version": "2.0.3",
"license": "(MIT OR WTFPL)",
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/expect": { "node_modules/expect": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
@@ -19589,7 +19671,6 @@
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.3", "version": "1.15.3",
"dev": true,
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -19897,6 +19978,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"license": "MIT",
"optional": true
},
"node_modules/fs-extra": { "node_modules/fs-extra": {
"version": "7.0.0", "version": "7.0.0",
"dev": true, "dev": true,
@@ -20111,6 +20197,11 @@
"assert-plus": "^1.0.0" "assert-plus": "^1.0.0"
} }
}, },
"node_modules/github-from-package": {
"version": "0.0.0",
"license": "MIT",
"optional": true
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"license": "ISC", "license": "ISC",
@@ -21242,7 +21333,7 @@
}, },
"node_modules/ieee754": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"dev": true, "devOptional": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -21431,7 +21522,7 @@
}, },
"node_modules/ini": { "node_modules/ini": {
"version": "1.3.8", "version": "1.3.8",
"dev": true, "devOptional": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/internal-slot": { "node_modules/internal-slot": {
@@ -21801,18 +21892,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-network-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
"integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-number": { "node_modules/is-number": {
"version": "3.0.0", "version": "3.0.0",
"license": "MIT", "license": "MIT",
@@ -27479,9 +27558,8 @@
} }
}, },
"node_modules/jsonwebtoken": { "node_modules/jsonwebtoken": {
"version": "9.0.2", "version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", "license": "MIT",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"dependencies": { "dependencies": {
"jws": "^3.2.2", "jws": "^3.2.2",
"lodash.includes": "^4.3.0", "lodash.includes": "^4.3.0",
@@ -27492,17 +27570,16 @@
"lodash.isstring": "^4.0.1", "lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0", "lodash.once": "^4.0.0",
"ms": "^2.1.1", "ms": "^2.1.1",
"semver": "^7.5.4" "semver": "^5.6.0"
}, },
"engines": { "engines": {
"node": ">=12", "node": ">=4",
"npm": ">=6" "npm": ">=1.4.28"
} }
}, },
"node_modules/jsonwebtoken/node_modules/jwa": { "node_modules/jsonwebtoken/node_modules/jwa": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", "license": "MIT",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": { "dependencies": {
"buffer-equal-constant-time": "1.0.1", "buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11", "ecdsa-sig-formatter": "1.0.11",
@@ -27511,24 +27588,12 @@
}, },
"node_modules/jsonwebtoken/node_modules/jws": { "node_modules/jsonwebtoken/node_modules/jws": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", "license": "MIT",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": { "dependencies": {
"jwa": "^1.4.1", "jwa": "^1.4.1",
"safe-buffer": "^5.0.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": { "node_modules/jsprim": {
"version": "1.4.2", "version": "1.4.2",
"license": "MIT", "license": "MIT",
@@ -27581,6 +27646,16 @@
"version": "2.6.0", "version": "2.6.0",
"license": "MIT" "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": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"license": "MIT", "license": "MIT",
@@ -27897,8 +27972,7 @@
}, },
"node_modules/lodash.includes": { "node_modules/lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "license": "MIT"
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
}, },
"node_modules/lodash.invokemap": { "node_modules/lodash.invokemap": {
"version": "4.6.0", "version": "4.6.0",
@@ -27907,8 +27981,7 @@
}, },
"node_modules/lodash.isboolean": { "node_modules/lodash.isboolean": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "license": "MIT"
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
}, },
"node_modules/lodash.isequal": { "node_modules/lodash.isequal": {
"version": "4.5.0", "version": "4.5.0",
@@ -27916,23 +27989,19 @@
}, },
"node_modules/lodash.isinteger": { "node_modules/lodash.isinteger": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "license": "MIT"
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
}, },
"node_modules/lodash.isnumber": { "node_modules/lodash.isnumber": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", "license": "MIT"
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
}, },
"node_modules/lodash.isplainobject": { "node_modules/lodash.isplainobject": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "license": "MIT"
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
}, },
"node_modules/lodash.isstring": { "node_modules/lodash.isstring": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "license": "MIT"
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
}, },
"node_modules/lodash.memoize": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
@@ -27944,8 +28013,7 @@
}, },
"node_modules/lodash.once": { "node_modules/lodash.once": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "license": "MIT"
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
}, },
"node_modules/lodash.pullall": { "node_modules/lodash.pullall": {
"version": "4.2.0", "version": "4.2.0",
@@ -29458,6 +29526,11 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"license": "MIT",
"optional": true
},
"node_modules/moment": { "node_modules/moment": {
"version": "2.29.4", "version": "2.29.4",
"license": "MIT", "license": "MIT",
@@ -29524,6 +29597,16 @@
"version": "2.1.3", "version": "2.1.3",
"license": "MIT" "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": { "node_modules/multicast-dns": {
"version": "7.2.5", "version": "7.2.5",
"dev": true, "dev": true,
@@ -29584,6 +29667,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/napi-build-utils": {
"version": "1.0.2",
"license": "MIT",
"optional": true
},
"node_modules/native-promise-only": { "node_modules/native-promise-only": {
"version": "0.8.1", "version": "0.8.1",
"dev": true, "dev": true,
@@ -29668,12 +29756,58 @@
"node": ">=12.0.0" "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": { "node_modules/node-abort-controller": {
"version": "3.1.1", "version": "3.1.1",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/node-addon-api": {
"version": "4.3.0",
"license": "MIT",
"optional": true
},
"node_modules/node-dir": { "node_modules/node-dir": {
"version": "0.1.17", "version": "0.1.17",
"dev": true, "dev": true,
@@ -30257,20 +30391,14 @@
} }
}, },
"node_modules/p-retry": { "node_modules/p-retry": {
"version": "6.2.1", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
"integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/retry": "0.12.2", "@types/retry": "0.12.0",
"is-network-error": "^1.0.0",
"retry": "^0.13.1" "retry": "^0.13.1"
}, },
"engines": { "engines": {
"node": ">=16.17" "node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-try": { "node_modules/p-try": {
@@ -30878,6 +31006,80 @@
"version": "4.2.0", "version": "4.2.0",
"license": "MIT" "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": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"license": "MIT", "license": "MIT",
@@ -31307,6 +31509,28 @@
"version": "0.5.1", "version": "0.5.1",
"dev": true "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": { "node_modules/re-resizable": {
"version": "6.9.11", "version": "6.9.11",
"license": "MIT", "license": "MIT",
@@ -34215,6 +34439,45 @@
"node": ">=10" "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": { "node_modules/tar/node_modules/chownr": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@@ -36017,13 +36280,6 @@
} }
} }
}, },
"node_modules/webpack-dev-server/node_modules/@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"dev": true,
"license": "MIT"
},
"node_modules/webpack-dev-server/node_modules/ajv": { "node_modules/webpack-dev-server/node_modules/ajv": {
"version": "8.12.0", "version": "8.12.0",
"dev": true, "dev": true,
@@ -36071,20 +36327,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/webpack-dev-server/node_modules/p-retry": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/retry": "0.12.0",
"retry": "^0.13.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/webpack-dev-server/node_modules/rimraf": { "node_modules/webpack-dev-server/node_modules/rimraf": {
"version": "3.0.2", "version": "3.0.2",
"dev": true, "dev": true,
@@ -36573,6 +36815,13 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true "dev": true
}, },
"node_modules/xpath.js": {
"version": "1.1.0",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"license": "MIT", "license": "MIT",

View File

@@ -7,7 +7,8 @@
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.2.0-beta.1", "@azure/cosmos": "4.2.0-beta.1",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0", "@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.1.1",
"@azure/msal-browser": "2.14.2", "@azure/msal-browser": "2.14.2",
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12", "@babel/plugin-proposal-decorators": "7.12.12",
@@ -81,7 +82,7 @@
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.44.0", "monaco-editor": "0.44.0",
"ms": "2.1.3", "ms": "2.1.3",
"p-retry": "6.2.1", "p-retry": "4.6.2",
"patch-package": "8.0.0", "patch-package": "8.0.0",
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42", "post-robot": "10.0.42",

View File

@@ -1,4 +1,5 @@
import { defineConfig, devices } from "@playwright/test"; import { defineConfig, devices } from "@playwright/test";
/** /**
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
*/ */
@@ -28,12 +29,7 @@ export default defineConfig({
projects: [ projects: [
{ {
name: "chromium", name: "chromium",
use: { use: { ...devices["Desktop Chrome"] },
...devices["Desktop Chrome"],
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
}, },
{ {
name: "firefox", name: "firefox",

View File

@@ -1,7 +1,7 @@
[defaults] [defaults]
group = dataexplorer-preview group = stfaul
sku = P1V2 sku = P1v2
appserviceplan = dataexplorer-preview appserviceplan = stfaul_asp_Linux_centralus_0
location = westus2 location = centralus
web = dataexplorer-preview web = cosmos-explorer-preview

View File

@@ -4,8 +4,8 @@ Cosmos Explorer Preview makes it possible to try a working version of any commit
Initial support is for Hosted (Connection string only) or the Azure Portal. Examples: Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
Connection string URLs: https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html Connection string URLs: https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions. In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions.

View File

@@ -1,4 +1,4 @@
{ {
"PROXY_PATH": "/proxy", "PROXY_PATH": "/proxy",
"msalRedirectURI": "https://dataexplorer-preview.azurewebsites.net/" "msalRedirectURI": "https://cosmos-explorer-preview.azurewebsites.net/"
} }

View File

@@ -3,15 +3,8 @@ const { createProxyMiddleware } = require("http-proxy-middleware");
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const fetch = require("node-fetch"); const fetch = require("node-fetch");
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com"; const api = createProxyMiddleware("/api", {
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net"; target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
const previewStorageWebsiteEndpoint = "https://dataexplorerpreview.z5.web.core.windows.net/";
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
const githubPullRequestUrl = "https://github.com/Azure/cosmos-explorer/pull";
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
const api = createProxyMiddleware({
target: backendEndpoint,
changeOrigin: true, changeOrigin: true,
logLevel: "debug", logLevel: "debug",
bypass: (req, res) => { bypass: (req, res) => {
@@ -22,8 +15,8 @@ const api = createProxyMiddleware({
}, },
}); });
const proxy = createProxyMiddleware({ const proxy = createProxyMiddleware("/proxy", {
target: backendEndpoint, target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
logLevel: "debug", logLevel: "debug",
@@ -34,38 +27,35 @@ const proxy = createProxyMiddleware({
}, },
}); });
const commit = createProxyMiddleware({ const commit = createProxyMiddleware("/commit", {
target: previewStorageWebsiteEndpoint, target: "https://cosmosexplorerpreview.blob.core.windows.net",
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
logLevel: "debug", logLevel: "debug",
pathRewrite: { "^/commit": "/" }, pathRewrite: { "^/commit": "$web/" },
}); });
const app = express(); const app = express();
app.use("/api", api); app.use(api);
app.use("/proxy", proxy); app.use(proxy);
app.use("/commit", commit); app.use(commit);
app.get("/pull/:pr(\\d+)", (req, res) => { app.get("/pull/:pr(\\d+)", (req, res) => {
const pr = req.params.pr; const pr = req.params.pr;
if (!/^\d+$/.test(pr)) {
return res.status(400).send("Invalid pull request number");
}
const [, query] = req.originalUrl.split("?"); const [, query] = req.originalUrl.split("?");
const search = new URLSearchParams(query); const search = new URLSearchParams(query);
fetch(`${githubApiUrl}/pulls/${pr}`) fetch("https://api.github.com/repos/Azure/cosmos-explorer/pulls/" + pr)
.then((response) => response.json()) .then((response) => response.json())
.then(({ head: { ref, sha } }) => { .then(({ head: { ref, sha } }) => {
const prUrl = new URL(`${githubPullRequestUrl}/${pr}`); const prUrl = new URL("https://github.com/Azure/cosmos-explorer/pull/" + pr);
prUrl.hash = ref; prUrl.hash = ref;
search.set("feature.pr", prUrl.href); search.set("feature.pr", prUrl.href);
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/explorer.html`); const explorer = new URL("https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/explorer.html");
explorer.search = search.toString(); explorer.search = search.toString();
const portal = new URL(azurePortalMpacEndpoint); const portal = new URL("https://ms.portal.azure.com/");
portal.searchParams.set("dataExplorerSource", explorer.href); portal.searchParams.set("dataExplorerSource", explorer.href);
return res.redirect(portal.href); return res.redirect(portal.href);
@@ -73,10 +63,12 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
.catch(() => res.sendStatus(500)); .catch(() => res.sendStatus(500));
}); });
app.get("/", (req, res) => { app.get("/", (req, res) => {
fetch(`${githubApiUrl}/branches/master`) fetch("https://api.github.com/repos/Azure/cosmos-explorer/branches/master")
.then((response) => response.json()) .then((response) => response.json())
.then(({ commit: { sha } }) => { .then(({ commit: { sha } }) => {
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/hostedExplorer.html`); const explorer = new URL(
"https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/hostedExplorer.html"
);
return res.redirect(explorer.href); return res.redirect(explorer.href);
}) })
.catch(() => res.sendStatus(500)); .catch(() => res.sendStatus(500));

37133
preview/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2", "deploy": "az webapp up --name \"cosmos-explorer-preview\" --subscription \"cosmosdb-portalteam-generaltest-msft\" --resource-group \"stfaul\"",
"start": "node index.js", "start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
@@ -12,8 +12,7 @@
"author": "Microsoft Corporation", "author": "Microsoft Corporation",
"dependencies": { "dependencies": {
"express": "^4.17.1", "express": "^4.17.1",
"http-proxy-middleware": "^3.0.3", "http-proxy-middleware": "^1.1.0",
"node": "^18.20.6",
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
} }
} }

View File

@@ -97,12 +97,6 @@ export enum CapacityMode {
Serverless = "Serverless", Serverless = "Serverless",
} }
export enum WorkloadType {
Learning = "Learning",
DevelopmentTesting = "Development/Testing",
Production = "Production",
None = "None",
}
// flight names returned from the portal are always lowercase // flight names returned from the portal are always lowercase
export class Flights { export class Flights {
public static readonly SettingsV2 = "settingsv2"; public static readonly SettingsV2 = "settingsv2";
@@ -125,7 +119,6 @@ export class AfecFeatures {
export class TagNames { export class TagNames {
public static defaultExperience: string = "defaultExperience"; public static defaultExperience: string = "defaultExperience";
public static WorkloadType: string = "hidden-workload-type";
} }
export class MongoDBAccounts { export class MongoDBAccounts {
@@ -525,18 +518,6 @@ export class PriorityLevel {
public static readonly Default = "low"; public static readonly Default = "low";
} }
export class ariaLabelForLearnMoreLink {
public static readonly AnalyticalStore = "Learn more about analytical store.";
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
}
export class GlobalSecondaryIndexLabels {
public static readonly NewGlobalSecondaryIndex: string = "New Global Secondary Index";
}
export class FeedbackLabels {
public static readonly provideFeedback: string = "Provide feedback";
}
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB"; export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
export const QueryCopilotSampleContainerId = "SampleContainer"; export const QueryCopilotSampleContainerId = "SampleContainer";

View File

@@ -1,15 +1,14 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens"; import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { AuthorizationToken } from "Contracts/FabricMessageTypes"; import { AuthorizationToken } from "Contracts/FabricMessageTypes";
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil"; import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { PriorityLevel } from "../Common/Constants"; import { BackendApi, PriorityLevel } from "../Common/Constants";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { Platform, configContext } from "../ConfigContext"; import { Platform, configContext } from "../ConfigContext";
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext"; import { updateUserContext, userContext } from "../UserContext";
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
@@ -20,7 +19,7 @@ const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo; const { verb, resourceId, resourceType, headers } = requestInfo;
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType); const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) { if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
Logger.logInfo( Logger.logInfo(
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `, `AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
@@ -43,7 +42,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
return decodeURIComponent(headers.authorization); return decodeURIComponent(headers.authorization);
} }
if (isFabricMirroredKey()) { if (configContext.platform === Platform.Fabric) {
switch (requestInfo.resourceType) { switch (requestInfo.resourceType) {
case Cosmos.ResourceType.conflicts: case Cosmos.ResourceType.conflicts:
case Cosmos.ResourceType.container: case Cosmos.ResourceType.container:
@@ -55,13 +54,8 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
// User resource tokens // User resource tokens
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined // TODO userContext.fabricContext.databaseConnectionInfo can be undefined
headers[HttpHeaders.msDate] = new Date().toUTCString(); headers[HttpHeaders.msDate] = new Date().toUTCString();
const resourceTokens = ( const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY] checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
).resourceTokenInfo.resourceTokens;
checkDatabaseResourceTokensValidity(
(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
.resourceTokenInfo.resourceTokensTimestamp,
);
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId); return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
case Cosmos.ResourceType.none: case Cosmos.ResourceType.none:
@@ -72,9 +66,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
// For now, these operations aren't used, so fetching the authorization token is commented out. // For now, these operations aren't used, so fetching the authorization token is commented out.
// This provider must return a real token to pass validation by the client, so we return the cached resource token // This provider must return a real token to pass validation by the client, so we return the cached resource token
// (which is a valid token, but won't work for these operations). // (which is a valid token, but won't work for these operations).
const resourceTokens2 = ( const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
).resourceTokenInfo.resourceTokens;
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId); return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
/* ************** TODO: Uncomment this code if we need to support these operations ************** /* ************** TODO: Uncomment this code if we need to support these operations **************
@@ -125,11 +117,7 @@ export const endpoint = () => {
const location = _global.parent ? _global.parent.location : _global.location; const location = _global.parent ? _global.parent.location : _global.location;
return configContext.EMULATOR_ENDPOINT || location.origin; return configContext.EMULATOR_ENDPOINT || location.origin;
} }
return ( return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
userContext.selectedRegionalEndpoint ||
userContext.endpoint ||
userContext?.databaseAccount?.properties?.documentEndpoint
);
}; };
export async function getTokenFromAuthService( export async function getTokenFromAuthService(
@@ -137,6 +125,10 @@ export async function getTokenFromAuthService(
resourceType: string, resourceType: string,
resourceId?: string, resourceId?: string,
): Promise<AuthorizationToken> { ): Promise<AuthorizationToken> {
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
}
try { try {
const host: string = configContext.PORTAL_BACKEND_ENDPOINT; const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", { const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
@@ -159,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 // The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
enum SDKSupportedCapabilities { enum SDKSupportedCapabilities {
None = 0, None = 0,
@@ -183,10 +203,8 @@ export function client(): Cosmos.CosmosClient {
} }
let _defaultHeaders: Cosmos.CosmosHeaders = {}; let _defaultHeaders: Cosmos.CosmosHeaders = {};
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] = _defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge; SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
_defaultHeaders["x-ms-cosmos-throughput-bucket"] = 1;
if ( if (
userContext.authType === AuthType.ConnectionString || userContext.authType === AuthType.ConnectionString ||
@@ -207,7 +225,6 @@ export function client(): Cosmos.CosmosClient {
userAgentSuffix: "Azure Portal", userAgentSuffix: "Azure Portal",
defaultHeaders: _defaultHeaders, defaultHeaders: _defaultHeaders,
connectionPolicy: { connectionPolicy: {
enableEndpointDiscovery: !userContext.selectedRegionalEndpoint,
retryOptions: { retryOptions: {
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts), maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval), fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),

View File

@@ -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);
});
});
});

View File

@@ -1,6 +1,3 @@
import { TagNames, WorkloadType } from "Common/Constants";
import { Tags } from "Contracts/DataModels";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
function isVirtualNetworkFilterEnabled() { function isVirtualNetworkFilterEnabled() {
@@ -18,18 +15,3 @@ function isPrivateEndpointConnectionsEnabled() {
export function isPublicInternetAccessAllowed(): boolean { export function isPublicInternetAccessAllowed(): boolean {
return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled(); return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled();
} }
export function getWorkloadType(): WorkloadType {
const tags: Tags = userContext?.databaseAccount?.tags;
const workloadType: WorkloadType = tags && (tags[TagNames.WorkloadType] as WorkloadType);
if (!workloadType) {
return WorkloadType.None;
}
return workloadType;
}
export function isGlobalSecondaryIndexEnabled(): boolean {
return (
!isFabric() && userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews
);
}

View File

@@ -1,8 +1,5 @@
import { QueryOperationOptions } from "@azure/cosmos"; // import { QueryOperationOptions } from "@azure/cosmos";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as Constants from "../Common/Constants";
import { QueryResults } from "../Contracts/ViewModels"; import { QueryResults } from "../Contracts/ViewModels";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
interface QueryResponse { interface QueryResponse {
// [Todo] remove any // [Todo] remove any
@@ -14,19 +11,13 @@ interface QueryResponse {
} }
export interface MinimalQueryIterator { export interface MinimalQueryIterator {
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>; fetchNext: () => Promise<QueryResponse>;
} }
// Pick<QueryIterator<any>, "fetchNext">; // Pick<QueryIterator<any>, "fetchNext">;
export function nextPage( export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
documentsIterator: MinimalQueryIterator, return documentsIterator.fetchNext().then((response) => {
firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> {
TelemetryProcessor.traceStart(Action.ExecuteQuery);
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab });
const documents = response.resources; const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any

View File

@@ -4,8 +4,16 @@ import { configContext, resetConfigContext, updateConfigContext } from "../Confi
import { DatabaseAccount } from "../Contracts/DataModels"; import { DatabaseAccount } from "../Contracts/DataModels";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; import {
deleteDocument,
getEndpoint,
getFeatureEndpointOrDefault,
queryDocuments,
readDocument,
updateDocument,
} from "./MongoProxyClient";
const databaseId = "testDB"; const databaseId = "testDB";
@@ -188,8 +196,20 @@ describe("MongoProxyClient", () => {
expect.any(Object), expect.any(Object),
); );
}); });
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_BACKEND_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
updateDocument(databaseId, collection, documentId, "{}");
expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
expect.any(Object),
);
});
}); });
describe("deleteDocuments", () => { describe("deleteDocument", () => {
beforeEach(() => { beforeEach(() => {
resetConfigContext(); resetConfigContext();
updateUserContext({ updateUserContext({
@@ -206,9 +226,9 @@ describe("MongoProxyClient", () => {
}); });
it("builds the correct URL", () => { it("builds the correct URL", () => {
deleteDocuments(databaseId, collection, [documentId]); deleteDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`, `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
expect.any(Object), expect.any(Object),
); );
}); });
@@ -218,9 +238,9 @@ describe("MongoProxyClient", () => {
MONGO_PROXY_ENDPOINT: "https://localhost:1234", MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [], globallyEnabledMongoAPIs: [],
}); });
deleteDocuments(databaseId, collection, [documentId]); deleteDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`, `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
expect.any(Object), expect.any(Object),
); );
}); });
@@ -255,4 +275,33 @@ describe("MongoProxyClient", () => {
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`); expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
}); });
}); });
describe("getFeatureEndpointOrDefault", () => {
beforeEach(() => {
resetConfigContext();
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
const params = new URLSearchParams({
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
"feature.mongoProxyAPIs": "readDocument|createDocument",
});
const features = extractFeatures(params);
updateUserContext({
authType: AuthType.AAD,
features: features,
});
});
it("returns a local endpoint", () => {
const endpoint = getFeatureEndpointOrDefault("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`);
});
});
}); });

View File

@@ -1,13 +1,20 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos"; import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import {
allowedMongoProxyEndpoints_ToBeDeprecated,
defaultAllowedMongoProxyEndpoints,
validateEndpoint,
} from "Utils/EndpointUtils";
import queryString from "querystring";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { hasFlag } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants"; import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants";
import { MinimalQueryIterator } from "./IteratorUtilities"; import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
@@ -60,6 +67,10 @@ export function queryDocuments(
query: string, query: string,
continuationToken?: string, continuationToken?: string,
): Promise<QueryResponse> { ): Promise<QueryResponse> {
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) {
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
}
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = { const params = {
@@ -78,7 +89,7 @@ export function queryDocuments(
query, query,
}; };
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || ""; const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || "";
const headers = { const headers = {
...defaultHeaders, ...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( export function readDocument(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
documentId: DocumentId, documentId: DocumentId,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) {
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
}
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/"); const idComponents = documentId.self.split("/");
@@ -141,7 +217,7 @@ export function readDocument(
: "", : "",
}; };
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument);
return window return window
.fetch(endpoint, { .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( export function createDocument(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
partitionKeyProperty: string, partitionKeyProperty: string,
documentContent: unknown, documentContent: unknown,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) {
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
}
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = { const params = {
@@ -183,7 +308,7 @@ export function createDocument(
documentContent: JSON.stringify(documentContent), documentContent: JSON.stringify(documentContent),
}; };
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument);
return window return window
.fetch(`${endpoint}/createDocument`, { .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( export function updateDocument(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
documentId: DocumentId, documentId: DocumentId,
documentContent: string, documentContent: string,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) {
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
}
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/"); const idComponents = documentId.self.split("/");
@@ -229,7 +396,7 @@ export function updateDocument(
: "", : "",
documentContent, documentContent,
}; };
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument);
return window return window
.fetch(endpoint, { .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( export function deleteDocuments(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
@@ -275,7 +575,7 @@ export function deleteDocuments(
resourceGroup: userContext.resourceGroup, resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name, databaseAccountName: databaseAccount.name,
}; };
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
return window return window
.fetch(`${endpoint}/bulkdelete`, { .fetch(`${endpoint}/bulkdelete`, {
@@ -299,6 +599,9 @@ export function deleteDocuments(
export function createMongoCollectionWithProxy( export function createMongoCollectionWithProxy(
params: DataModels.CreateCollectionParams, params: DataModels.CreateCollectionParams,
): Promise<DataModels.Collection> { ): Promise<DataModels.Collection> {
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) {
return createMongoCollectionWithProxy_ToBeDeprecated(params);
}
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const shardKey: string = params.partitionKey?.paths[0]; const shardKey: string = params.partitionKey?.paths[0];
@@ -319,7 +622,7 @@ export function createMongoCollectionWithProxy(
isSharded: !!shardKey, isSharded: !!shardKey,
}; };
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy);
return window return window
.fetch(`${endpoint}/createCollection`, { .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 { export function getEndpoint(endpoint: string): string {
let url = endpoint + "/api/mongo/explorer"; let url = endpoint + "/api/mongo/explorer";
@@ -352,6 +719,84 @@ export function getEndpoint(endpoint: string): string {
return url; return url;
} }
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
[MongoProxyApi.ResourceList]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.QueryDocuments]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.CreateDocument]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.ReadDocument]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.UpdateDocument]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.DeleteDocument]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.CreateCollectionWithProxy]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.LegacyMongoShell]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.BulkDelete]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
};
if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) {
return false;
}
if (configContext.globallyEnabledMongoAPIs.includes(mongoProxyApi)) {
return true;
}
return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT);
}
export class ThrottlingError extends Error { export class ThrottlingError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);

View File

@@ -1,10 +1,10 @@
import { isFabric } from "Platform/Fabric/FabricUtil"; import { Platform, configContext } from "../ConfigContext";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less"); export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
export function updateStyles(): void { export function updateStyles(): void {
if (isFabric()) { if (configContext.platform === Platform.Fabric) {
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh; StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium; StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
StyleConstants.AccentLight = StyleConstants.FabricAccentLight; StyleConstants.AccentLight = StyleConstants.FabricAccentLight;

View File

@@ -1,5 +1,4 @@
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos"; import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases"; import { useDatabases } from "../../Explorer/useDatabases";
@@ -25,7 +24,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
); );
try { try {
let collection: DataModels.Collection; let collection: DataModels.Collection;
if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
if (params.createNewDatabase) { if (params.createNewDatabase) {
const createDatabaseParams: DataModels.CreateDatabaseParams = { const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: params.autoPilotMaxThroughput, autoPilotMaxThroughput: params.autoPilotMaxThroughput,

View File

@@ -1,74 +0,0 @@
import { constructRpOptions } from "Common/dataAccess/createCollection";
import { handleError } from "Common/ErrorHandlingUtils";
import { Collection, CreateMaterializedViewsParams as CreateGlobalSecondaryIndexParams } from "Contracts/DataModels";
import { userContext } from "UserContext";
import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources";
import {
CreateUpdateOptions,
SqlContainerResource,
SqlDatabaseCreateUpdateParameters,
} from "Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
export const createGlobalSecondaryIndex = async (params: CreateGlobalSecondaryIndexParams): Promise<Collection> => {
const clearMessage = logConsoleProgress(
`Creating a new global secondary index ${params.materializedViewId} for database ${params.databaseId}`,
);
const options: CreateUpdateOptions = constructRpOptions(params);
const resource: SqlContainerResource = {
id: params.materializedViewId,
};
if (params.materializedViewDefinition) {
resource.materializedViewDefinition = params.materializedViewDefinition;
}
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.indexingPolicy) {
resource.indexingPolicy = params.indexingPolicy;
}
if (params.partitionKey) {
resource.partitionKey = params.partitionKey;
}
if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
}
if (params.vectorEmbeddingPolicy) {
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
}
if (params.fullTextPolicy) {
resource.fullTextPolicy = params.fullTextPolicy;
}
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource,
options,
},
};
try {
const createResponse = await createUpdateSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.materializedViewId,
rpPayload,
);
logConsoleInfo(`Successfully created global secondary index ${params.materializedViewId}`);
return createResponse && (createResponse.properties.resource as Collection);
} catch (error) {
handleError(
error,
"CreateGlobalSecondaryIndex",
`Error while creating global secondary index ${params.materializedViewId}`,
);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,4 +1,3 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
@@ -13,7 +12,7 @@ import { handleError } from "../ErrorHandlingUtils";
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> { export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`); const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
try { try {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) { if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
await deleteCollectionWithARM(databaseId, collectionId); await deleteCollectionWithARM(databaseId, collectionId);
} else { } else {
await client().database(databaseId).container(collectionId).delete(); await client().database(databaseId).container(collectionId).delete();

View File

@@ -1,4 +1,4 @@
import { QueryOperationOptions } from "@azure/cosmos"; // import { QueryOperationOptions } from "@azure/cosmos";
import { QueryResults } from "../../Contracts/ViewModels"; import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { getEntityName } from "../DocumentUtility"; import { getEntityName } from "../DocumentUtility";
@@ -9,13 +9,13 @@ export const queryDocumentsPage = async (
resourceName: string, resourceName: string,
documentsIterator: MinimalQueryIterator, documentsIterator: MinimalQueryIterator,
firstItemIndex: number, firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions, // queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> => { ): Promise<QueryResults> => {
const entityName = getEntityName(); const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
try { try {
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions); const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
const itemCount = (result.documents && result.documents.length) || 0; const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result; return result;

View File

@@ -1,4 +1,3 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels"; import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -14,11 +13,6 @@ import { readOfferWithSDK } from "./readOfferWithSDK";
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => { export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`); const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
if (isFabric()) {
// Not exposing offers in Fabric
return undefined;
}
try { try {
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
@@ -111,8 +105,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
? parseInt(resource.softAllowedMaximumThroughput) ? parseInt(resource.softAllowedMaximumThroughput)
: resource.softAllowedMaximumThroughput; : resource.softAllowedMaximumThroughput;
const throughputBuckets = resource?.throughputBuckets;
if (autoscaleSettings) { if (autoscaleSettings) {
return { return {
id: offerId, id: offerId,
@@ -122,7 +114,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
offerReplacePending: resource.offerReplacePending === "true", offerReplacePending: resource.offerReplacePending === "true",
instantMaximumThroughput, instantMaximumThroughput,
softAllowedMaximumThroughput, softAllowedMaximumThroughput,
throughputBuckets,
}; };
} }
@@ -134,7 +125,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
offerReplacePending: resource.offerReplacePending === "true", offerReplacePending: resource.offerReplacePending === "true",
instantMaximumThroughput, instantMaximumThroughput,
softAllowedMaximumThroughput, softAllowedMaximumThroughput,
throughputBuckets,
}; };
} }

View File

@@ -1,10 +1,9 @@
import { ContainerResponse } from "@azure/cosmos"; import { ContainerResponse } from "@azure/cosmos";
import { Queries } from "Common/Constants"; import { Queries } from "Common/Constants";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; import { Platform, configContext } from "ConfigContext";
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { FabricArtifactInfo, userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
@@ -17,13 +16,15 @@ import { handleError } from "../ErrorHandlingUtils";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> { export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) { if (
configContext.platform === Platform.Fabric &&
userContext.fabricContext &&
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
) {
const collections: DataModels.Collection[] = []; const collections: DataModels.Collection[] = [];
const promises: Promise<ContainerResponse>[] = []; const promises: Promise<ContainerResponse>[] = [];
for (const collectionResourceId in ( for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
).resourceTokenInfo.resourceTokens) {
// Dictionary key looks like this: dbs/SampleDB/colls/Container // Dictionary key looks like this: dbs/SampleDB/colls/Container
const resourceIdObj = collectionResourceId.split("/"); const resourceIdObj = collectionResourceId.split("/");
const tokenDatabaseId = resourceIdObj[1]; const tokenDatabaseId = resourceIdObj[1];
@@ -55,8 +56,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" && userContext.apiType !== "Tables"
!isFabric()
) { ) {
return await readCollectionsWithARM(databaseId); return await readCollectionsWithARM(databaseId);
} }
@@ -126,12 +126,5 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
throw new Error(`Unsupported default experience type: ${apiType}`); throw new Error(`Unsupported default experience type: ${apiType}`);
} }
// TO DO: Remove when we get RP API Spec with materializedViews return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
/* eslint-disable @typescript-eslint/no-explicit-any */
return rpResponse?.value?.map((collection: any) => {
const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection;
collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews;
collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition;
return collectionDataModel;
});
} }

View File

@@ -1,4 +1,4 @@
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil"; import { Platform, configContext } from "ConfigContext";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels"; import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -11,9 +11,8 @@ import { handleError } from "../ErrorHandlingUtils";
import { readOfferWithSDK } from "./readOfferWithSDK"; import { readOfferWithSDK } from "./readOfferWithSDK";
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => { export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
if (isFabricMirroredKey() || isFabricNative()) { if (configContext.platform === Platform.Fabric) {
// For Fabric Mirroring, it is slow, because it requests the token and we don't need it. // TODO This works, but is very slow, because it requests the token, so we skip for now
// For Fabric Native, it is not supported.
console.error("Skiping readDatabaseOffer for Fabric"); console.error("Skiping readDatabaseOffer for Fabric");
return undefined; return undefined;
} }
@@ -24,8 +23,7 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" && userContext.apiType !== "Tables"
!isFabric()
) { ) {
return await readDatabaseOfferWithARM(params.databaseId); return await readDatabaseOfferWithARM(params.databaseId);
} }

View File

@@ -1,8 +1,7 @@
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; import { Platform, configContext } from "ConfigContext";
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { FabricArtifactInfo, userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
@@ -15,13 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[]; let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`); const clearMessage = logConsoleProgress(`Querying databases`);
if ( if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
isFabricMirroredKey() && const tokensData = userContext.fabricContext.databaseConnectionInfo;
(userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo
.resourceTokens
) {
const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
.resourceTokenInfo;
const databaseIdsSet = new Set<string>(); // databaseId const databaseIdsSet = new Set<string>(); // databaseId
@@ -52,28 +46,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
})); }));
clearMessage(); clearMessage();
return databases; return databases;
} else if (isFabricNative() && userContext.fabricContext?.databaseName) {
const databaseId = userContext.fabricContext.databaseName;
databases = [
{
_rid: "",
_self: "",
_etag: "",
_ts: 0,
id: databaseId,
collections: [],
},
];
clearMessage();
return databases;
} }
try { try {
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" && userContext.apiType !== "Tables"
!isFabric()
) { ) {
databases = await readDatabasesWithARM(); databases = await readDatabasesWithARM();
} else { } else {

View File

@@ -1,5 +1,4 @@
import { ContainerDefinition, RequestOptions } from "@azure/cosmos"; import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels"; import { Collection } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -37,8 +36,7 @@ export async function updateCollection(
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" && userContext.apiType !== "Tables"
!isFabric()
) { ) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection); collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
} else { } else {

View File

@@ -1,7 +1,6 @@
import { OfferDefinition, RequestOptions } from "@azure/cosmos"; import { OfferDefinition, RequestOptions } from "@azure/cosmos";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels"; import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { import {
migrateCassandraKeyspaceToAutoscale, migrateCassandraKeyspaceToAutoscale,
@@ -57,7 +56,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`); const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
try { try {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) { if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
if (params.collectionId) { if (params.collectionId) {
updatedOffer = await updateCollectionOfferWithARM(params); updatedOffer = await updateCollectionOfferWithARM(params);
} else if (userContext.apiType === "Tables") { } else if (userContext.apiType === "Tables") {
@@ -360,13 +359,6 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
body.properties.resource.throughput = params.manualThroughput; body.properties.resource.throughput = params.manualThroughput;
} }
if (params.throughputBuckets) {
const throughputBuckets = params.throughputBuckets.filter(
(bucket: ThroughputBucket) => bucket.maxThroughputPercentage !== 100,
);
body.properties.resource.throughputBuckets = throughputBuckets;
}
return body; return body;
}; };

View File

@@ -12,6 +12,7 @@ import {
allowedGraphEndpoints, allowedGraphEndpoints,
allowedHostedExplorerEndpoints, allowedHostedExplorerEndpoints,
allowedJunoOrigins, allowedJunoOrigins,
allowedMongoBackendEndpoints,
allowedMsalRedirectEndpoints, allowedMsalRedirectEndpoints,
defaultAllowedArmEndpoints, defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints, defaultAllowedBackendEndpoints,
@@ -49,8 +50,10 @@ export interface ConfigContext {
CATALOG_API_KEY: string; CATALOG_API_KEY: string;
ARCADIA_ENDPOINT: string; ARCADIA_ENDPOINT: string;
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
BACKEND_ENDPOINT?: string;
PORTAL_BACKEND_ENDPOINT: string; PORTAL_BACKEND_ENDPOINT: string;
NEW_BACKEND_APIS?: BackendApi[]; NEW_BACKEND_APIS?: BackendApi[];
MONGO_BACKEND_ENDPOINT?: string;
MONGO_PROXY_ENDPOINT: string; MONGO_PROXY_ENDPOINT: string;
CASSANDRA_PROXY_ENDPOINT: string; CASSANDRA_PROXY_ENDPOINT: string;
NEW_CASSANDRA_APIS?: string[]; NEW_CASSANDRA_APIS?: string[];
@@ -88,7 +91,7 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/.*\\.analysis-df\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
`^https:\\/\\/.*\\.azure-test\\.net$`, `^https:\\/\\/.*\\.azure-test\\.net$`,
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`, `^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net$`,
], // Webpack injects this at build time ], // Webpack injects this at build time
gitSha: process.env.GIT_SHA, gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/", hostedExplorerURL: "https://cosmos.azure.com/",
@@ -106,6 +109,7 @@ let configContext: Readonly<ConfigContext> = {
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306 GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
JUNO_ENDPOINT: JunoEndpoints.Prod, JUNO_ENDPOINT: JunoEndpoints.Prod,
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
@@ -148,6 +152,15 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.ARCADIA_ENDPOINT; delete newContext.ARCADIA_ENDPOINT;
} }
if (
!validateEndpoint(
newContext.BACKEND_ENDPOINT,
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
)
) {
delete newContext.BACKEND_ENDPOINT;
}
if ( if (
!validateEndpoint( !validateEndpoint(
newContext.MONGO_PROXY_ENDPOINT, newContext.MONGO_PROXY_ENDPOINT,
@@ -157,6 +170,10 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.MONGO_PROXY_ENDPOINT; delete newContext.MONGO_PROXY_ENDPOINT;
} }
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
delete newContext.MONGO_BACKEND_ENDPOINT;
}
if ( if (
!validateEndpoint( !validateEndpoint(
newContext.CASSANDRA_PROXY_ENDPOINT, newContext.CASSANDRA_PROXY_ENDPOINT,

View File

@@ -6,7 +6,6 @@ export interface ArmEntity {
location: string; location: string;
type: string; type: string;
kind: string; kind: string;
tags?: Tags;
} }
export interface DatabaseAccount extends ArmEntity { export interface DatabaseAccount extends ArmEntity {
@@ -32,7 +31,6 @@ export interface DatabaseAccountExtendedProperties {
writeLocations?: DatabaseAccountResponseLocation[]; writeLocations?: DatabaseAccountResponseLocation[];
enableFreeTier?: boolean; enableFreeTier?: boolean;
enableAnalyticalStorage?: boolean; enableAnalyticalStorage?: boolean;
enableMaterializedViews?: boolean;
isVirtualNetworkFilterEnabled?: boolean; isVirtualNetworkFilterEnabled?: boolean;
ipRules?: IpRule[]; ipRules?: IpRule[];
privateEndpointConnections?: unknown[]; privateEndpointConnections?: unknown[];
@@ -165,8 +163,6 @@ export interface Collection extends Resource {
schema?: ISchema; schema?: ISchema;
requestSchema?: () => void; requestSchema?: () => void;
computedProperties?: ComputedProperties; computedProperties?: ComputedProperties;
materializedViews?: MaterializedView[];
materializedViewDefinition?: MaterializedViewDefinition;
} }
export interface CollectionsWithPagination { export interface CollectionsWithPagination {
@@ -226,17 +222,6 @@ export interface ComputedProperty {
export type ComputedProperties = ComputedProperty[]; export type ComputedProperties = ComputedProperty[];
export interface MaterializedView {
id: string;
_rid: string;
}
export interface MaterializedViewDefinition {
definition: string;
sourceCollectionId: string;
sourceCollectionRid?: string;
}
export interface PartitionKey { export interface PartitionKey {
paths: string[]; paths: string[];
kind: "Hash" | "Range" | "MultiHash"; kind: "Hash" | "Range" | "MultiHash";
@@ -289,12 +274,6 @@ export interface Offer {
offerReplacePending: boolean; offerReplacePending: boolean;
instantMaximumThroughput?: number; instantMaximumThroughput?: number;
softAllowedMaximumThroughput?: number; softAllowedMaximumThroughput?: number;
throughputBuckets?: ThroughputBucket[];
}
export interface ThroughputBucket {
id: number;
maxThroughputPercentage: number;
} }
export interface SDKOfferDefinition extends Resource { export interface SDKOfferDefinition extends Resource {
@@ -359,7 +338,9 @@ export interface CreateDatabaseParams {
offerThroughput?: number; offerThroughput?: number;
} }
export interface CreateCollectionParamsBase { export interface CreateCollectionParams {
createNewDatabase: boolean;
collectionId: string;
databaseId: string; databaseId: string;
databaseLevelThroughput: boolean; databaseLevelThroughput: boolean;
offerThroughput?: number; offerThroughput?: number;
@@ -373,16 +354,6 @@ export interface CreateCollectionParamsBase {
fullTextPolicy?: FullTextPolicy; fullTextPolicy?: FullTextPolicy;
} }
export interface CreateCollectionParams extends CreateCollectionParamsBase {
createNewDatabase: boolean;
collectionId: string;
}
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
materializedViewId: string;
materializedViewDefinition: MaterializedViewDefinition;
}
export interface VectorEmbeddingPolicy { export interface VectorEmbeddingPolicy {
vectorEmbeddings: VectorEmbedding[]; vectorEmbeddings: VectorEmbedding[];
} }
@@ -425,7 +396,6 @@ export interface UpdateOfferParams {
collectionId?: string; collectionId?: string;
migrateToAutoPilot?: boolean; migrateToAutoPilot?: boolean;
migrateToManual?: boolean; migrateToManual?: boolean;
throughputBuckets?: ThroughputBucket[];
} }
export interface Notification { export interface Notification {
@@ -693,5 +663,3 @@ export interface FeatureRegistration {
state: string; state: string;
}; };
} }
export type Tags = { [key: string]: string };

View File

@@ -4,7 +4,6 @@
export enum FabricMessageTypes { export enum FabricMessageTypes {
GetAuthorizationToken = "GetAuthorizationToken", GetAuthorizationToken = "GetAuthorizationToken",
GetAllResourceTokens = "GetAllResourceTokens", GetAllResourceTokens = "GetAllResourceTokens",
GetAccessToken = "GetAccessToken",
Ready = "Ready", Ready = "Ready",
} }

View File

@@ -1,9 +1,47 @@
import { AuthorizationToken } from "./FabricMessageTypes"; import { AuthorizationToken } from "Contracts/FabricMessageTypes";
// This is the version of these messages // This is the version of these messages
export const FABRIC_RPC_VERSION = "FabricMessageV3"; export const FABRIC_RPC_VERSION = "2";
// Fabric to Data Explorer // Fabric to Data Explorer
// TODO Deprecated. Remove this section once DE is updated
export type FabricMessageV1 =
| {
type: "newContainer";
databaseName: string;
}
| {
type: "initialize";
message: {
endpoint: string | undefined;
databaseId: string | undefined;
resourceTokens: unknown | undefined;
resourceTokensTimestamp: number | undefined;
error: string | undefined;
};
}
| {
type: "authorizationToken";
message: {
id: string;
error: string | undefined;
data: AuthorizationToken | undefined;
};
}
| {
type: "allResourceTokens";
message: {
id: string;
error: string | undefined;
endpoint: string | undefined;
databaseId: string | undefined;
resourceTokens: unknown | undefined;
resourceTokensTimestamp: number | undefined;
};
};
// -----------------------------
export type FabricMessageV2 = export type FabricMessageV2 =
| { | {
type: "newContainer"; type: "newContainer";
@@ -31,7 +69,7 @@ export type FabricMessageV2 =
message: { message: {
id: string; id: string;
error: string | undefined; error: string | undefined;
data: ResourceTokenInfo | undefined; data: FabricDatabaseConnectionInfo | undefined;
}; };
} }
| { | {
@@ -41,88 +79,17 @@ export type FabricMessageV2 =
}; };
}; };
export type FabricMessageV3 = export type CosmosDBTokenResponse = {
| { token: string;
type: "newContainer"; date: string;
databaseName: string; };
}
| {
type: "initialize";
version: string;
id: string;
message: InitializeMessageV3<CosmosDbArtifactType>;
}
| {
type: "authorizationToken";
message: {
id: string;
error: string | undefined;
data: AuthorizationToken | undefined;
};
}
| {
type: "allResourceTokens_v2";
message: {
id: string;
error: string | undefined;
data: ResourceTokenInfo | undefined;
};
}
| {
type: "explorerVisible";
message: {
visible: boolean;
};
}
| {
type: "accessToken";
message: {
id: string;
error: string | undefined;
data: { accessToken: string };
};
}
| {
type: "refreshResourceTree";
message: {
id: string;
error: string | undefined;
};
};
export enum CosmosDbArtifactType { export type CosmosDBConnectionInfoResponse = {
MIRRORED_KEY = "MIRRORED_KEY",
MIRRORED_AAD = "MIRRORED_AAD",
NATIVE = "NATIVE",
}
export interface ArtifactConnectionInfo {
[CosmosDbArtifactType.MIRRORED_KEY]: { connectionId: string };
[CosmosDbArtifactType.MIRRORED_AAD]: AccessTokenConnectionInfo;
[CosmosDbArtifactType.NATIVE]: AccessTokenConnectionInfo;
}
export interface AccessTokenConnectionInfo {
accessToken: string;
databaseName: string;
accountEndpoint: string;
}
export interface InitializeMessageV3<T extends CosmosDbArtifactType> {
connectionId: string;
isVisible: boolean;
isReadOnly: boolean;
artifactType: T;
artifactConnectionInfo: ArtifactConnectionInfo[T];
}
export interface CosmosDBConnectionInfoResponse {
endpoint: string; endpoint: string;
databaseId: string; databaseId: string;
resourceTokens: Record<string, string> | undefined; resourceTokens: { [resourceId: string]: string };
accessToken: string | undefined; };
isReadOnly: boolean;
credentialType: "Key" | "OAuth2" | undefined;
}
export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse { export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
resourceTokensTimestamp: number; resourceTokensTimestamp: number;
} }

View File

@@ -41,7 +41,7 @@ export enum MessageTypes {
OpenPostgreSQLPasswordReset, OpenPostgreSQLPasswordReset,
OpenPostgresNetworkingBlade, OpenPostgresNetworkingBlade,
OpenCosmosDBNetworkingBlade, OpenCosmosDBNetworkingBlade,
DisplayNPSSurvey, // unused DisplayNPSSurvey,
OpenVCoreMongoNetworkingBlade, OpenVCoreMongoNetworkingBlade,
OpenVCoreMongoConnectionStringsBlade, OpenVCoreMongoConnectionStringsBlade,
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums. GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.

View File

@@ -1,5 +1,4 @@
import { import {
JSONObject,
QueryMetrics, QueryMetrics,
Resource, Resource,
StoredProcedureDefinition, StoredProcedureDefinition,
@@ -144,8 +143,6 @@ export interface Collection extends CollectionBase {
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>; geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
documentIds: ko.ObservableArray<DocumentId>; documentIds: ko.ObservableArray<DocumentId>;
computedProperties: ko.Observable<DataModels.ComputedProperties>; computedProperties: ko.Observable<DataModels.ComputedProperties>;
materializedViews: ko.Observable<DataModels.MaterializedView[]>;
materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
cassandraKeys: CassandraTableKeys; cassandraKeys: CassandraTableKeys;
cassandraSchema: CassandraTableKey[]; cassandraSchema: CassandraTableKey[];
@@ -207,12 +204,6 @@ export interface Collection extends CollectionBase {
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void; onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void; onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>; uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
bulkInsertDocuments(documents: JSONObject[]): Promise<{
numSucceeded: number;
numFailed: number;
numThrottled: number;
errors: string[];
}>;
} }
/** /**
@@ -415,6 +406,7 @@ export interface DataExplorerInputsFrame {
csmEndpoint?: string; csmEndpoint?: string;
dnsSuffix?: string; dnsSuffix?: string;
serverId?: string; serverId?: string;
extensionEndpoint?: string;
portalBackendEndpoint?: string; portalBackendEndpoint?: string;
mongoProxyEndpoint?: string; mongoProxyEndpoint?: string;
cassandraProxyEndpoint?: string; cassandraProxyEndpoint?: string;

View File

@@ -1,13 +1,5 @@
import { GlobalSecondaryIndexLabels } from "Common/Constants";
import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility";
import { configContext, Platform } from "ConfigContext";
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import {
AddGlobalSecondaryIndexPanel,
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
@@ -27,6 +19,7 @@ import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { Platform, configContext } from "./../ConfigContext";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook"; import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
@@ -48,7 +41,7 @@ export interface DatabaseContextMenuButtonParams {
* New resource tree (in ReactJS) * New resource tree (in ReactJS)
*/ */
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => { export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
if (isFabric() && userContext.fabricContext?.isReadOnly) { if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
return undefined; return undefined;
} }
@@ -60,7 +53,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
}, },
]; ];
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) { if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
items.push({ items.push({
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
@@ -152,7 +145,7 @@ export const createCollectionContextMenuButton = (
}); });
} }
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) { if (configContext.platform !== Platform.Fabric) {
items.push({ items.push({
iconSrc: DeleteCollectionIcon, iconSrc: DeleteCollectionIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
@@ -170,24 +163,6 @@ export const createCollectionContextMenuButton = (
}); });
} }
if (isGlobalSecondaryIndexEnabled() && !selectedCollection.materializedViewDefinition()) {
items.push({
label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
onClick: () => {
const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = {
explorer: container,
sourceContainer: selectedCollection,
};
useSidePanel
.getState()
.openSidePanel(
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
<AddGlobalSecondaryIndexPanel {...addMaterializedViewPanelProps} />,
);
},
});
}
return items; return items;
}; };

View File

@@ -214,10 +214,8 @@ export const Dialog: FC = () => {
{contentHtml} {contentHtml}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />} {progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter> <DialogFooter>
<PrimaryButton {...primaryButtonProps} data-test={`DialogButton:${primaryButtonText}`} /> <PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && ( {secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
<DefaultButton {...secondaryButtonProps} data-test={`DialogButton:${secondaryButtonText}`} />
)}
</DialogFooter> </DialogFooter>
</FluentDialog> </FluentDialog>
) : ( ) : (

View File

@@ -1,7 +1,5 @@
import { AuthType } from "AuthType";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import ko from "knockout"; import ko from "knockout";
import { Features } from "Platform/Hosted/extractFeatures";
import React from "react"; import React from "react";
import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
@@ -249,42 +247,4 @@ describe("SettingsComponent", () => {
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom); expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath); expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
}); });
it("should save throughput bucket changes when Save button is clicked", async () => {
updateUserContext({
apiType: "SQL",
features: { enableThroughputBuckets: true } as Features,
authType: AuthType.AAD,
});
const wrapper = shallow(<SettingsComponent {...baseProps} />);
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
const isEnabled = settingsComponentInstance["throughputBucketsEnabled"];
expect(isEnabled).toBe(true);
wrapper.setState({
isThroughputBucketsSaveable: true,
throughputBuckets: [
{ id: 1, maxThroughputPercentage: 70 },
{ id: 2, maxThroughputPercentage: 60 },
],
});
await settingsComponentInstance.onSaveClick();
expect(updateOffer).toHaveBeenCalledWith({
databaseId: collection.databaseId,
collectionId: collection.id(),
currentOffer: expect.any(Object),
autopilotThroughput: collection.offer().autoscaleMaxThroughput,
manualThroughput: collection.offer().manualThroughput,
throughputBuckets: [
{ id: 1, maxThroughputPercentage: 70 },
{ id: 2, maxThroughputPercentage: 60 },
],
});
expect(wrapper.state("isThroughputBucketsSaveable")).toBe(false);
});
}); });

View File

@@ -7,12 +7,7 @@ import {
ContainerPolicyComponent, ContainerPolicyComponent,
ContainerPolicyComponentProps, ContainerPolicyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent"; } from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
import {
ThroughputBucketsComponent,
ThroughputBucketsComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react"; import * as React from "react";
@@ -45,10 +40,6 @@ import {
ConflictResolutionComponent, ConflictResolutionComponent,
ConflictResolutionComponentProps, ConflictResolutionComponentProps,
} from "./SettingsSubComponents/ConflictResolutionComponent"; } from "./SettingsSubComponents/ConflictResolutionComponent";
import {
GlobalSecondaryIndexComponent,
GlobalSecondaryIndexComponentProps,
} from "./SettingsSubComponents/GlobalSecondaryIndexComponent";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent"; import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import { import {
MongoIndexingPolicyComponent, MongoIndexingPolicyComponent,
@@ -95,8 +86,6 @@ export interface SettingsComponentState {
wasAutopilotOriginallySet: boolean; wasAutopilotOriginallySet: boolean;
isScaleSaveable: boolean; isScaleSaveable: boolean;
isScaleDiscardable: boolean; isScaleDiscardable: boolean;
throughputBuckets: DataModels.ThroughputBucket[];
throughputBucketsBaseline: DataModels.ThroughputBucket[];
throughputError: string; throughputError: string;
timeToLive: TtlType; timeToLive: TtlType;
@@ -115,7 +104,6 @@ export interface SettingsComponentState {
changeFeedPolicyBaseline: ChangeFeedPolicyState; changeFeedPolicyBaseline: ChangeFeedPolicyState;
isSubSettingsSaveable: boolean; isSubSettingsSaveable: boolean;
isSubSettingsDiscardable: boolean; isSubSettingsDiscardable: boolean;
isThroughputBucketsSaveable: boolean;
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy; vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy; vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
@@ -167,11 +155,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private shouldShowComputedPropertiesEditor: boolean; private shouldShowComputedPropertiesEditor: boolean;
private shouldShowIndexingPolicyEditor: boolean; private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean; private shouldShowPartitionKeyEditor: boolean;
private isGlobalSecondaryIndex: boolean;
private isVectorSearchEnabled: boolean; private isVectorSearchEnabled: boolean;
private isFullTextSearchEnabled: boolean; private isFullTextSearchEnabled: boolean;
private totalThroughputUsed: number; private totalThroughputUsed: number;
private throughputBucketsEnabled: boolean;
public mongoDBCollectionResource: MongoDBCollectionResource; public mongoDBCollectionResource: MongoDBCollectionResource;
constructor(props: SettingsComponentProps) { constructor(props: SettingsComponentProps) {
@@ -185,16 +171,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL"; this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo"; this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud(); this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.isGlobalSecondaryIndex =
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection); this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection); this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
this.throughputBucketsEnabled =
userContext.apiType === "SQL" &&
userContext.features.enableThroughputBuckets &&
userContext.authType === AuthType.AAD;
// Mongo container with system partition key still treat as "Fixed" // Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer = this.isFixedContainer =
@@ -213,8 +193,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
wasAutopilotOriginallySet: false, wasAutopilotOriginallySet: false,
isScaleSaveable: false, isScaleSaveable: false,
isScaleDiscardable: false, isScaleDiscardable: false,
throughputBuckets: undefined,
throughputBucketsBaseline: undefined,
throughputError: undefined, throughputError: undefined,
timeToLive: undefined, timeToLive: undefined,
@@ -233,7 +211,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
changeFeedPolicyBaseline: undefined, changeFeedPolicyBaseline: undefined,
isSubSettingsSaveable: false, isSubSettingsSaveable: false,
isSubSettingsDiscardable: false, isSubSettingsDiscardable: false,
isThroughputBucketsSaveable: false,
vectorEmbeddingPolicy: undefined, vectorEmbeddingPolicy: undefined,
vectorEmbeddingPolicyBaseline: undefined, vectorEmbeddingPolicyBaseline: undefined,
@@ -350,8 +327,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isIndexingPolicyDirty || this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty || this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty || this.state.isComputedPropertiesDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) || (!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
this.state.isThroughputBucketsSaveable
); );
}; };
@@ -363,8 +339,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isIndexingPolicyDirty || this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty || this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty || this.state.isComputedPropertiesDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) || (!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
this.state.isThroughputBucketsSaveable
); );
}; };
@@ -444,8 +419,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ this.setState({
throughput: this.state.throughputBaseline, throughput: this.state.throughputBaseline,
throughputBuckets: this.state.throughputBucketsBaseline,
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
timeToLive: this.state.timeToLiveBaseline, timeToLive: this.state.timeToLiveBaseline,
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline, timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline, displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
@@ -468,7 +441,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isScaleSaveable: false, isScaleSaveable: false,
isScaleDiscardable: false, isScaleDiscardable: false,
isSubSettingsSaveable: false, isSubSettingsSaveable: false,
isThroughputBucketsSaveable: false,
isSubSettingsDiscardable: false, isSubSettingsDiscardable: false,
isContainerPolicyDirty: false, isContainerPolicyDirty: false,
isIndexingPolicyDirty: false, isIndexingPolicyDirty: false,
@@ -507,10 +479,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void => private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
this.setState({ indexingPolicyContent: newIndexingPolicy }); this.setState({ indexingPolicyContent: newIndexingPolicy });
private onThroughputBucketsSaveableChange = (isSaveable: boolean): void => {
this.setState({ isThroughputBucketsSaveable: isSaveable });
};
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false }); private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false }); private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
@@ -781,13 +749,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
] as DataModels.ComputedProperties; ] as DataModels.ComputedProperties;
} }
const throughputBuckets = this.offer?.throughputBuckets;
return { return {
throughput: offerThroughput, throughput: offerThroughput,
throughputBaseline: offerThroughput, throughputBaseline: offerThroughput,
throughputBuckets,
throughputBucketsBaseline: throughputBuckets,
changeFeedPolicy: changeFeedPolicy, changeFeedPolicy: changeFeedPolicy,
changeFeedPolicyBaseline: changeFeedPolicy, changeFeedPolicyBaseline: changeFeedPolicy,
timeToLive: timeToLive, timeToLive: timeToLive,
@@ -875,10 +839,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ throughput: newThroughput, throughputError }); this.setState({ throughput: newThroughput, throughputError });
}; };
private onThroughputBucketChange = (throughputBuckets: DataModels.ThroughputBucket[]): void => {
this.setState({ throughputBuckets });
};
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void => private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
this.setState({ isAutoPilotSelected: isAutoPilotSelected }); this.setState({ isAutoPilotSelected: isAutoPilotSelected });
@@ -1069,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) { if (this.state.isScaleSaveable) {
const updateOfferParams: DataModels.UpdateOfferParams = { const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId, databaseId: this.collection.databaseId,
@@ -1267,23 +1209,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange, onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
}; };
const throughputBucketsComponentProps: ThroughputBucketsComponentProps = {
currentBuckets: this.state.throughputBuckets,
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
onBucketsChange: this.onThroughputBucketChange,
onSaveableChange: this.onThroughputBucketsSaveableChange,
};
const partitionKeyComponentProps: PartitionKeyComponentProps = { const partitionKeyComponentProps: PartitionKeyComponentProps = {
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId), database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
collection: this.collection, collection: this.collection,
explorer: this.props.settingsTab.getContainer(), explorer: this.props.settingsTab.getContainer(),
isReadOnly: isFabricNative(),
};
const globalSecondaryIndexComponentProps: GlobalSecondaryIndexComponentProps = {
collection: this.collection,
explorer: this.props.settingsTab.getContainer(),
}; };
const tabs: SettingsV2TabInfo[] = []; const tabs: SettingsV2TabInfo[] = [];
@@ -1342,20 +1271,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}); });
} }
if (this.throughputBucketsEnabled) {
tabs.push({
tab: SettingsV2TabTypes.ThroughputBucketsTab,
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
});
}
if (this.isGlobalSecondaryIndex) {
tabs.push({
tab: SettingsV2TabTypes.GlobalSecondaryIndexTab,
content: <GlobalSecondaryIndexComponent {...globalSecondaryIndexComponentProps} />,
});
}
const pivotProps: IPivotProps = { const pivotProps: IPivotProps = {
onLinkClick: this.onPivotChange, onLinkClick: this.onPivotChange,
selectedKey: SettingsV2TabTypes[this.state.selectedTab], selectedKey: SettingsV2TabTypes[this.state.selectedTab],

View File

@@ -1,46 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { collection, container } from "../TestUtils";
import { GlobalSecondaryIndexComponent } from "./GlobalSecondaryIndexComponent";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
describe("GlobalSecondaryIndexComponent", () => {
let testCollection: typeof collection;
let testExplorer: typeof container;
beforeEach(() => {
testCollection = { ...collection };
});
it("renders only the source component when materializedViewDefinition is missing", () => {
testCollection.materializedViews([
{ id: "view1", _rid: "rid1" },
{ id: "view2", _rid: "rid2" },
]);
testCollection.materializedViewDefinition(null);
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(true);
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false);
});
it("renders only the target component when materializedViews is missing", () => {
testCollection.materializedViews(null);
testCollection.materializedViewDefinition({
definition: "SELECT * FROM c WHERE c.id = 1",
sourceCollectionId: "source1",
sourceCollectionRid: "rid123",
});
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false);
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(true);
});
it("renders neither component when both are missing", () => {
testCollection.materializedViews(null);
testCollection.materializedViewDefinition(null);
const wrapper = shallow(<GlobalSecondaryIndexComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false);
expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false);
});
});

View File

@@ -1,41 +0,0 @@
import { FontIcon, Link, Stack, Text } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
export interface GlobalSecondaryIndexComponentProps {
collection: ViewModels.Collection;
explorer: Explorer;
}
export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexComponentProps> = ({
collection,
explorer,
}) => {
const isTargetContainer = !!collection?.materializedViewDefinition();
const isSourceContainer = !!collection?.materializedViews();
return (
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
{isSourceContainer && (
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following indexes defined for it.</Text>
)}
<Text>
<Link
target="_blank"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
>
Learn more
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
</Link>{" "}
about how to define global secondary indexes and how to use them.
</Text>
</Stack>
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}
{isTargetContainer && <GlobalSecondaryIndexTargetComponent collection={collection} />}
</Stack>
);
};

View File

@@ -1,42 +0,0 @@
import { PrimaryButton } from "@fluentui/react";
import { shallow } from "enzyme";
import React from "react";
import { collection, container } from "../TestUtils";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
describe("GlobalSecondaryIndexSourceComponent", () => {
let testCollection: typeof collection;
let testExplorer: typeof container;
beforeEach(() => {
testCollection = { ...collection };
});
it("renders without crashing", () => {
const wrapper = shallow(
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
);
expect(wrapper.exists()).toBe(true);
});
it("renders the PrimaryButton", () => {
const wrapper = shallow(
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
);
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
});
it("updates when new global secondary indexes are provided", () => {
const wrapper = shallow(
<GlobalSecondaryIndexSourceComponent collection={testCollection} explorer={testExplorer} />,
);
// Simulating an update by modifying the observable directly
testCollection.materializedViews([{ id: "view3", _rid: "rid3" }]);
wrapper.setProps({ collection: testCollection });
wrapper.update();
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
});
});

View File

@@ -1,114 +0,0 @@
import { PrimaryButton } from "@fluentui/react";
import { GlobalSecondaryIndexLabels } from "Common/Constants";
import { MaterializedView } from "Contracts/DataModels";
import Explorer from "Explorer/Explorer";
import { loadMonaco } from "Explorer/LazyMonaco";
import { AddGlobalSecondaryIndexPanel } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { useDatabases } from "Explorer/useDatabases";
import { useSidePanel } from "hooks/useSidePanel";
import * as monaco from "monaco-editor";
import React, { useEffect, useRef } from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
export interface GlobalSecondaryIndexSourceComponentProps {
collection: ViewModels.Collection;
explorer: Explorer;
}
export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexSourceComponentProps> = ({
collection,
explorer,
}) => {
const editorContainerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
const globalSecondaryIndexes: MaterializedView[] = collection?.materializedViews() ?? [];
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedView[] with collection id.
const getViewDetails = (viewId: string): { definition: string; partitionKey: string[] } => {
let definition = "";
let partitionKey: string[] = [];
useDatabases.getState().databases.find((database) => {
const collection = database.collections().find((collection) => collection.id() === viewId);
if (collection) {
const globalSecondaryIndexDefinition = collection.materializedViewDefinition();
globalSecondaryIndexDefinition && (definition = globalSecondaryIndexDefinition.definition);
collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths);
}
});
return { definition, partitionKey };
};
//JSON value for the editor using the fetched id and definitions.
const jsonValue = JSON.stringify(
globalSecondaryIndexes.map((view) => {
const { definition, partitionKey } = getViewDetails(view.id);
return {
name: view.id,
partitionKey: partitionKey.join(", "),
definition,
};
}),
null,
2,
);
// Initialize Monaco editor with the computed JSON value.
useEffect(() => {
let disposed = false;
const initMonaco = async () => {
const monacoInstance = await loadMonaco();
if (disposed || !editorContainerRef.current) {
return;
}
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
value: jsonValue,
language: "json",
ariaLabel: "Global Secondary Index JSON",
readOnly: true,
});
};
initMonaco();
return () => {
disposed = true;
editorRef.current?.dispose();
};
}, [jsonValue]);
// Update the editor when the jsonValue changes.
useEffect(() => {
if (editorRef.current) {
editorRef.current.setValue(jsonValue);
}
}, [jsonValue]);
return (
<div>
<div
ref={editorContainerRef}
style={{
height: 250,
border: "1px solid #ccc",
borderRadius: 4,
overflow: "hidden",
}}
/>
<PrimaryButton
text="Add index"
styles={{ root: { width: "fit-content", marginTop: 12 } }}
onClick={() =>
useSidePanel
.getState()
.openSidePanel(
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
<AddGlobalSecondaryIndexPanel explorer={explorer} sourceContainer={collection} />,
)
}
/>
</div>
);
};

View File

@@ -1,32 +0,0 @@
import { Text } from "@fluentui/react";
import { Collection } from "Contracts/ViewModels";
import { shallow } from "enzyme";
import React from "react";
import { collection } from "../TestUtils";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
describe("GlobalSecondaryIndexTargetComponent", () => {
let testCollection: Collection;
beforeEach(() => {
testCollection = {
...collection,
materializedViewDefinition: collection.materializedViewDefinition,
};
});
it("renders without crashing", () => {
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
expect(wrapper.exists()).toBe(true);
});
it("displays the source container ID", () => {
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
});
it("displays the global secondary index definition", () => {
const wrapper = shallow(<GlobalSecondaryIndexTargetComponent collection={testCollection} />);
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
});
});

View File

@@ -1,45 +0,0 @@
import { Stack, Text } from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
export interface GlobalSecondaryIndexTargetComponentProps {
collection: ViewModels.Collection;
}
export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexTargetComponentProps> = ({
collection,
}) => {
const globalSecondaryIndexDefinition = collection?.materializedViewDefinition();
const textHeadingStyle = {
root: { fontWeight: "600", fontSize: 16 },
};
const valueBoxStyle = {
root: {
backgroundColor: "#f3f3f3",
padding: "5px 10px",
borderRadius: "4px",
},
};
return (
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
<Text styles={textHeadingStyle}>Global Secondary Index Settings</Text>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
</Stack>
</Stack>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Global secondary index definition</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.definition}</Text>
</Stack>
</Stack>
</Stack>
);
};

View File

@@ -14,7 +14,6 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers"; import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
import { Platform, configContext } from "ConfigContext";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane"; import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
import { import {
@@ -29,26 +28,16 @@ export interface PartitionKeyComponentProps {
database: ViewModels.Database; database: ViewModels.Database;
collection: ViewModels.Collection; collection: ViewModels.Collection;
explorer: Explorer; explorer: Explorer;
isReadOnly?: boolean; // true: cannot change partition key
} }
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ database, collection, explorer }) => {
database,
collection,
explorer,
isReadOnly,
}) => {
const { dataTransferJobs } = useDataTransferJobs(); const { dataTransferJobs } = useDataTransferJobs();
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null); const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
React.useEffect(() => { React.useEffect(() => {
if (isReadOnly) {
return;
}
const loadDataTransferJobs = refreshDataTransferOperations; const loadDataTransferJobs = refreshDataTransferOperations;
loadDataTransferJobs(); loadDataTransferJobs();
}, [isReadOnly]); }, []);
React.useEffect(() => { React.useEffect(() => {
const currentJob = findPortalDataTransferJob(); const currentJob = findPortalDataTransferJob();
@@ -173,61 +162,54 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
</Stack> </Stack>
</Stack> </Stack>
</Stack> </Stack>
<MessageBar messageBarType={MessageBarType.warning}>
{!isReadOnly && ( To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the
<> source container for the entire duration of the partition key change process.
<MessageBar messageBarType={MessageBarType.warning}> <Link
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
the source container for the entire duration of the partition key change process. target="_blank"
<Link underline
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work" >
target="_blank" Learn more
underline </Link>
> </MessageBar>
Learn more <Text>
</Link> To change the partition key, a new destination container must be created or an existing destination container
</MessageBar> selected. Data will then be copied to the destination container.
<Text> </Text>
To change the partition key, a new destination container must be created or an existing destination <PrimaryButton
container selected. Data will then be copied to the destination container. styles={{ root: { width: "fit-content" } }}
</Text> text="Change"
{configContext.platform !== Platform.Emulator && ( onClick={startPartitionkeyChangeWorkflow}
<PrimaryButton disabled={isCurrentJobInProgress(portalDataTransferJob)}
styles={{ root: { width: "fit-content" } }} />
text="Change" {portalDataTransferJob && (
onClick={startPartitionkeyChangeWorkflow} <Stack>
disabled={isCurrentJobInProgress(portalDataTransferJob)} <Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
/> <Stack
)} horizontal
{portalDataTransferJob && ( tokens={{ childrenGap: 20 }}
<Stack> styles={{
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text> root: {
<Stack alignItems: "center",
horizontal },
tokens={{ childrenGap: 20 }} }}
styles={{ >
root: { <ProgressIndicator
alignItems: "center", label={portalDataTransferJob?.properties?.jobName}
}, description={getProgressDescription()}
}} percentComplete={getPercentageComplete()}
> styles={{
<ProgressIndicator root: {
label={portalDataTransferJob?.properties?.jobName} width: "85%",
description={getProgressDescription()} },
percentComplete={getPercentageComplete()} }}
styles={{ ></ProgressIndicator>
root: { {isCurrentJobInProgress(portalDataTransferJob) && (
width: "85%", <DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
}, )}
}} </Stack>
></ProgressIndicator> </Stack>
{isCurrentJobInProgress(portalDataTransferJob) && (
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
)}
</Stack>
</Stack>
)}
</>
)} )}
</Stack> </Stack>
); );

View File

@@ -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);
});
});

View File

@@ -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>
);
};

View File

@@ -17,13 +17,14 @@ import {
} from "@fluentui/react"; } from "@fluentui/react";
import React from "react"; import React from "react";
import * as DataModels from "../../../../../Contracts/DataModels"; import * as DataModels from "../../../../../Contracts/DataModels";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import * as SharedConstants from "../../../../../Shared/Constants"; import * as SharedConstants from "../../../../../Shared/Constants";
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../../../UserContext"; import { userContext } from "../../../../../UserContext";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils"; import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
import { calculateEstimateNumber } from "../../../../../Utils/PricingUtils"; import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { import {
PriceBreakdown, PriceBreakdown,
@@ -365,6 +366,29 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
}); });
}; };
private minRUperGBSurvey = (): JSX.Element => {
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
const oneTBinKB = 1000000000;
const minRUperGB = 10;
const featureFlagEnabled = userContext.features.showMinRUSurvey;
const collectionIsEligible =
userContext.subscriptionType !== SubscriptionType.Internal &&
this.props.usageSizeInKB > oneTBinKB &&
this.props.minimum >= usageInGB(this.props.usageSizeInKB) * minRUperGB;
if (featureFlagEnabled || collectionIsEligible) {
return (
<Text>
Need to scale below {this.props.minimum} RU/s? Reach out by filling{" "}
<a target="_blank" rel="noreferrer" href={href}>
this questionnaire
</a>
.
</Text>
);
}
return undefined;
};
private renderThroughputModeChoices = (): JSX.Element => { private renderThroughputModeChoices = (): JSX.Element => {
const labelId = "settingsV2RadioButtonLabelId"; const labelId = "settingsV2RadioButtonLabelId";
return ( return (
@@ -637,6 +661,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
</Link> </Link>
</Text> </Text>
)} )}
{this.minRUperGBSurvey()}
{this.props.spendAckVisible && ( {this.props.spendAckVisible && (
<Checkbox <Checkbox
id="spendAckCheckBox" id="spendAckCheckBox"

View File

@@ -11,8 +11,7 @@ export type isDirtyTypes =
| DataModels.IndexingPolicy | DataModels.IndexingPolicy
| DataModels.ComputedProperties | DataModels.ComputedProperties
| DataModels.VectorEmbedding[] | DataModels.VectorEmbedding[]
| DataModels.FullTextPolicy | DataModels.FullTextPolicy;
| DataModels.ThroughputBucket[];
export const TtlOff = "off"; export const TtlOff = "off";
export const TtlOn = "on"; export const TtlOn = "on";
export const TtlOnNoDefault = "on-nodefault"; export const TtlOnNoDefault = "on-nodefault";
@@ -56,8 +55,6 @@ export enum SettingsV2TabTypes {
PartitionKeyTab, PartitionKeyTab,
ComputedPropertiesTab, ComputedPropertiesTab,
ContainerVectorPolicyTab, ContainerVectorPolicyTab,
ThroughputBucketsTab,
GlobalSecondaryIndexTab,
} }
export enum ContainerPolicyTabTypes { export enum ContainerPolicyTabTypes {
@@ -170,10 +167,6 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
return "Computed Properties"; return "Computed Properties";
case SettingsV2TabTypes.ContainerVectorPolicyTab: case SettingsV2TabTypes.ContainerVectorPolicyTab:
return "Container Policies"; return "Container Policies";
case SettingsV2TabTypes.ThroughputBucketsTab:
return "Throughput Buckets";
case SettingsV2TabTypes.GlobalSecondaryIndexTab:
return "Global Secondary Index (Preview)";
default: default:
throw new Error(`Unknown tab ${tab}`); throw new Error(`Unknown tab ${tab}`);
} }

View File

@@ -48,15 +48,6 @@ export const collection = {
]), ]),
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy), vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy), fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
materializedViews: ko.observable<DataModels.MaterializedView[]>([
{ id: "view1", _rid: "rid1" },
{ id: "view2", _rid: "rid2" },
]),
materializedViewDefinition: ko.observable<DataModels.MaterializedViewDefinition>({
definition: "SELECT * FROM c WHERE c.id = 1",
sourceCollectionId: "source1",
sourceCollectionRid: "rid123",
}),
readSettings: () => { readSettings: () => {
return; return;
}, },

View File

@@ -60,8 +60,6 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function], "getDatabase": [Function],
"id": [Function], "id": [Function],
"indexingPolicy": [Function], "indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function], "offer": [Function],
"partitionKey": { "partitionKey": {
"kind": "hash", "kind": "hash",
@@ -141,8 +139,6 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function], "getDatabase": [Function],
"id": [Function], "id": [Function],
"indexingPolicy": [Function], "indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function], "offer": [Function],
"partitionKey": { "partitionKey": {
"kind": "hash", "kind": "hash",
@@ -262,8 +258,6 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function], "getDatabase": [Function],
"id": [Function], "id": [Function],
"indexingPolicy": [Function], "indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function], "offer": [Function],
"partitionKey": { "partitionKey": {
"kind": "hash", "kind": "hash",
@@ -306,7 +300,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
} }
} }
isReadOnly={false}
/> />
</PivotItem> </PivotItem>
<PivotItem <PivotItem
@@ -343,101 +336,6 @@ exports[`SettingsComponent renders 1`] = `
shouldDiscardComputedProperties={false} shouldDiscardComputedProperties={false}
/> />
</PivotItem> </PivotItem>
<PivotItem
headerText="Global Secondary Index (Preview)"
itemKey="GlobalSecondaryIndexTab"
key="GlobalSecondaryIndexTab"
style={
{
"marginTop": 20,
}
}
>
<GlobalSecondaryIndexComponent
collection={
{
"analyticalStorageTtl": [Function],
"changeFeedPolicy": [Function],
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
"databaseId": "test",
"defaultTtl": [Function],
"fullTextPolicy": [Function],
"geospatialConfig": [Function],
"getDatabase": [Function],
"id": [Function],
"indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function],
"partitionKey": {
"kind": "hash",
"paths": [],
"version": 2,
},
"partitionKeyProperties": [
"partitionKey",
],
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
}
explorer={
Explorer {
"_isInitializingNotebooks": false,
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
}
/>
</PivotItem>
</StyledPivot> </StyledPivot>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,4 @@
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react"; import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
import { getWorkloadType } from "Common/DatabaseAccountUtility";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
@@ -35,23 +34,10 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
setIsThroughputCapExceeded, setIsThroughputCapExceeded,
onCostAcknowledgeChange, onCostAcknowledgeChange,
}: ThroughputInputProps) => { }: ThroughputInputProps) => {
let defaultThroughput: number;
const workloadType: Constants.WorkloadType = getWorkloadType();
if (
isFreeTier ||
isQuickstart ||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
} else if (workloadType === Constants.WorkloadType.Production) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
} else {
defaultThroughput = AutoPilotUtils.autoPilotThroughput4K;
}
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true); const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(defaultThroughput); const [throughput, setThroughput] = useState<number>(
isFreeTier || isQuickstart ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K,
);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false); const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>(""); const [throughputError, setThroughputError] = useState<string>("");
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0); const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
@@ -61,6 +47,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1; const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
useEffect(() => { useEffect(() => {
// throughput cap check for the initial state // throughput cap check for the initial state
let totalThroughput = 0; let totalThroughput = 0;
@@ -170,6 +157,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => { const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") { if (mode === "Autoscale") {
const defaultThroughput = isFreeTier
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K;
setThroughput(defaultThroughput); setThroughput(defaultThroughput);
setIsAutoScaleSelected(true); setIsAutoScaleSelected(true);
setThroughputValue(defaultThroughput); setThroughputValue(defaultThroughput);

View File

@@ -6,7 +6,6 @@ import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
// TODO: this does not seem to be used. Remove?
export class DataSamplesUtil { export class DataSamplesUtil {
private static readonly DialogTitle = "Create Sample Container"; private static readonly DialogTitle = "Create Sample Container";
constructor(private container: Explorer) {} constructor(private container: Explorer) {}

View File

@@ -8,7 +8,7 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { IGalleryItem } from "Juno/JunoClient"; import { IGalleryItem } from "Juno/JunoClient";
import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
@@ -35,7 +35,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings"; import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext, userContext } from "../UserContext"; import { isAccountNewerThanThresholdInMs, updateUserContext, userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
@@ -43,7 +43,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { ReactTabKind, useTabs } from "../hooks/useTabs"; import { useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog"; import { DialogProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
@@ -55,7 +55,7 @@ import type NotebookManager from "./Notebook/NotebookManager";
import { NotebookPaneContent } from "./Notebook/NotebookManager"; import { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookUtil } from "./Notebook/NotebookUtil";
import { useNotebook } from "./Notebook/useNotebook"; import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
@@ -187,10 +187,6 @@ export default class Explorer {
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath); useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
} }
if (isFabricMirrored()) {
useTabs.getState().closeReactTab(ReactTabKind.Home);
}
this.refreshExplorer(); this.refreshExplorer();
} }
@@ -282,6 +278,37 @@ export default class Explorer {
} }
} }
public openNPSSurveyDialog(): void {
if (!Platform.Portal || !["Postgres", "SQL", "Mongo"].includes(userContext.apiType)) {
return;
}
const ONE_DAY_IN_MS = 86400000;
const SEVEN_DAYS_IN_MS = 604800000;
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
if (userContext.isTryCosmosDBSubscription) {
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
Logger.logInfo(
`Sending message to Portal to check if NPS Survey can be displayed in Try Cosmos DB ${userContext.apiType}`,
"Explorer/openNPSSurveyDialog",
);
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
}
} else {
// Show survey when an existing account is older than 7 days
if (
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", SEVEN_DAYS_IN_MS)
) {
Logger.logInfo(
`Sending message to Portal to check if NPS Survey can be displayed for existing ${userContext.apiType} account older than 7 days`,
"Explorer/openNPSSurveyDialog",
);
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
}
}
}
public async openCESCVAFeedbackBlade(): Promise<void> { public async openCESCVAFeedbackBlade(): Promise<void> {
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade }); sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
Logger.logInfo( Logger.logInfo(
@@ -351,8 +378,8 @@ export default class Explorer {
}; };
public onRefreshResourcesClick = async (): Promise<void> => { public onRefreshResourcesClick = async (): Promise<void> => {
if (isFabricMirroredKey()) { if (configContext.platform === Platform.Fabric) {
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases()); scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
return; return;
} }
@@ -1131,7 +1158,7 @@ export default class Explorer {
await this.initNotebooks(userContext.databaseAccount); await this.initNotebooks(userContext.databaseAccount);
} }
this.refreshSampleData(); await this.refreshSampleData();
} }
public async configureCopilot(): Promise<void> { public async configureCopilot(): Promise<void> {
@@ -1156,27 +1183,26 @@ export default class Explorer {
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled); .setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
} }
public refreshSampleData(): void { public async refreshSampleData(): Promise<void> {
if (!userContext.sampleDataConnectionInfo) { try {
if (!userContext.sampleDataConnectionInfo) {
return;
}
const collection: DataModels.Collection = await readSampleCollection();
if (!collection) {
return;
}
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
if (!databaseId) {
return;
}
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
useDatabases.setState({ sampleDataResourceTokenCollection });
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer");
return; return;
} }
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
if (!databaseId) {
return;
}
readSampleCollection()
.then((collection: DataModels.Collection) => {
if (!collection) {
return;
}
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
useDatabases.setState({ sampleDataResourceTokenCollection });
})
.catch((error) => {
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData");
});
} }
} }

View File

@@ -14,6 +14,10 @@
.flex-direction(@direction: row); .flex-direction(@direction: row);
padding: 4px 5px; padding: 4px 5px;
label {
padding: 0px;
}
.valueCol { .valueCol {
flex-grow: 1; flex-grow: 1;
padding-right: 5px; padding-right: 5px;
@@ -59,10 +63,6 @@
height: 100%; height: 100%;
} }
.customTrashIcon {
padding-top: 33px;
}
.rightPaneTrashIconImg { .rightPaneTrashIconImg {
vertical-align: top; vertical-align: top;
} }

View File

@@ -142,11 +142,10 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
<div className="labelCol"> <div className="labelCol">
<TextField <TextField
className="edgeInput" className="edgeInput"
label={index === 0 && "Key"}
type="text" type="text"
id="propertyKeyNewVertexPane" id="propertyKeyNewVertexPane"
componentRef={input} componentRef={input}
required aria-required="true"
placeholder="Key" placeholder="Key"
autoComplete="off" autoComplete="off"
aria-label={`Enter value for propery ${index + 1}`} aria-label={`Enter value for propery ${index + 1}`}
@@ -154,11 +153,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)} onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
/> />
</div> </div>
<span className="mandatoryStar">*&nbsp;</span>
<div className="valueCol"> <div className="valueCol">
<TextField <TextField
className="edgeInput" className="edgeInput"
label={index === 0 && "Value"}
type="text" type="text"
placeholder="Value" placeholder="Value"
autoComplete="off" autoComplete="off"
@@ -170,8 +169,6 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
<div> <div>
<Dropdown <Dropdown
role="combobox" role="combobox"
label={index === 0 && "Type"}
ariaLabel="Type"
placeholder="Select an option" placeholder="Select an option"
defaultSelectedKey={data.values[0].type} defaultSelectedKey={data.values[0].type}
style={{ width: 100 }} style={{ width: 100 }}
@@ -184,7 +181,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
</div> </div>
<div className="actionCol"> <div className="actionCol">
<div <div
className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`} className="rightPaneTrashIcon rightPaneBtns"
tabIndex={0} tabIndex={0}
role="button" role="button"
aria-label={`Delete ${data.key}`} aria-label={`Delete ${data.key}`}

View File

@@ -6,12 +6,12 @@
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import * as React from "react"; import * as React from "react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants"; import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants"; import { StyleConstants } from "../../../Common/StyleConstants";
import { Platform, configContext } from "../../../ConfigContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
@@ -93,18 +93,19 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
); );
} }
const rootStyle = isFabric() const rootStyle =
? { configContext.platform === Platform.Fabric
root: { ? {
backgroundColor: "transparent", root: {
padding: "2px 8px 0px 8px", backgroundColor: "transparent",
}, padding: "2px 8px 0px 8px",
} },
: { }
root: { : {
backgroundColor: backgroundColor, root: {
}, backgroundColor: backgroundColor,
}; },
};
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);

View File

@@ -37,25 +37,21 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(enableAzureSynapseLinkBtn).toBeDefined(); expect(enableAzureSynapseLinkBtn).toBeDefined();
}); });
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the it("Button should not be visible for Tables API", () => {
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be updateUserContext({
// unsupported in jest and needs to be tested with react-hooks-testing-library. databaseAccount: {
// properties: {
// it("Button should not be visible for Tables API", () => { capabilities: [{ name: "EnableTable" }],
// updateUserContext({ },
// databaseAccount: { } as DatabaseAccount,
// properties: { });
// capabilities: [{ name: "EnableTable" }],
// }, const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
// } as DatabaseAccount, const enableAzureSynapseLinkBtn = buttons.find(
// }); (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
// );
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); expect(enableAzureSynapseLinkBtn).toBeUndefined();
// const enableAzureSynapseLinkBtn = buttons.find( });
// (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
// );
// expect(enableAzureSynapseLinkBtn).toBeUndefined();
//});
it("Button should not be visible for Cassandra API", () => { it("Button should not be visible for Cassandra API", () => {
updateUserContext({ updateUserContext({

View File

@@ -1,5 +1,4 @@
import { KeyboardAction } from "KeyboardShortcuts"; import { KeyboardAction } from "KeyboardShortcuts";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import * as React from "react"; import * as React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg"; import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
@@ -62,7 +61,7 @@ export function createStaticCommandBarButtons(
} }
} }
if (isDataplaneRbacSupported(userContext.apiType)) { if (userContext.apiType === "SQL") {
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined); const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled); const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated); const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);

View File

@@ -36,10 +36,6 @@
&:active { &:active {
background-color:@NotificationHigh; background-color:@NotificationHigh;
} }
&:focus {
.focusedBorder();
}
.statusBar { .statusBar {
.dataTypeIcons { .dataTypeIcons {

View File

@@ -81,6 +81,10 @@ export class NotificationConsoleComponent extends React.Component<
} }
} }
public setElememntRef = (element: HTMLElement): void => {
this.consoleHeaderElement = element;
};
public render(): JSX.Element { public render(): JSX.Element {
const numInProgress = this.state.allConsoleData.filter( const numInProgress = this.state.allConsoleData.filter(
(data: ConsoleData) => data.type === ConsoleDataType.InProgress, (data: ConsoleData) => data.type === ConsoleDataType.InProgress,
@@ -97,9 +101,7 @@ export class NotificationConsoleComponent extends React.Component<
<div <div
className="notificationConsoleHeader" className="notificationConsoleHeader"
id="notificationConsoleHeader" id="notificationConsoleHeader"
role="button" ref={this.setElememntRef}
aria-label="Console"
aria-expanded={this.props.isConsoleExpanded}
onClick={() => this.expandCollapseConsole()} onClick={() => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)} onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0} tabIndex={0}
@@ -107,15 +109,15 @@ export class NotificationConsoleComponent extends React.Component<
<div className="statusBar"> <div className="statusBar">
<span className="dataTypeIcons"> <span className="dataTypeIcons">
<span className="notificationConsoleHeaderIconWithData"> <span className="notificationConsoleHeaderIconWithData">
<img src={LoadingIcon} alt="In progress items" /> <img src={LoadingIcon} alt="in progress items" />
<span className="numInProgress">{numInProgress}</span> <span className="numInProgress">{numInProgress}</span>
</span> </span>
<span className="notificationConsoleHeaderIconWithData"> <span className="notificationConsoleHeaderIconWithData">
<img src={ErrorBlackIcon} alt="Error items" /> <img src={ErrorBlackIcon} alt="error items" />
<span className="numErroredItems">{numErroredItems}</span> <span className="numErroredItems">{numErroredItems}</span>
</span> </span>
<span className="notificationConsoleHeaderIconWithData"> <span className="notificationConsoleHeaderIconWithData">
<img src={infoBubbleIcon} alt="Info items" /> <img src={infoBubbleIcon} alt="info items" />
<span className="numInfoItems">{numInfoItems}</span> <span className="numInfoItems">{numInfoItems}</span>
</span> </span>
</span> </span>
@@ -127,10 +129,17 @@ export class NotificationConsoleComponent extends React.Component<
</span> </span>
</span> </span>
</div> </div>
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"> <div
className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton"
role="button"
tabIndex={0}
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
aria-expanded={!this.props.isConsoleExpanded}
>
<img <img
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon} src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"} alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
/> />
</div> </div>
</div> </div>
@@ -250,6 +259,9 @@ export class NotificationConsoleComponent extends React.Component<
} }
private onConsoleWasExpanded = (): void => { private onConsoleWasExpanded = (): void => {
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
this.consoleHeaderElement.focus();
}
useNotificationConsole.getState().setConsoleAnimationFinished(true); useNotificationConsole.getState().setConsoleAnimationFinished(true);
}; };

View File

@@ -5,13 +5,10 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleContainer" className="notificationConsoleContainer"
> >
<div <div
aria-expanded={false}
aria-label="Console"
className="notificationConsoleHeader" className="notificationConsoleHeader"
id="notificationConsoleHeader" id="notificationConsoleHeader"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
tabIndex={0} tabIndex={0}
> >
<div <div
@@ -24,7 +21,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="In progress items" alt="in progress items"
src={{}} src={{}}
/> />
<span <span
@@ -37,7 +34,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="Error items" alt="error items"
src={{}} src={{}}
/> />
<span <span
@@ -50,7 +47,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="Info items" alt="info items"
src={{}} src={{}}
/> />
<span <span
@@ -74,11 +71,15 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
</span> </span>
</div> </div>
<div <div
aria-expanded={true}
aria-label="console button collapsed"
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"
role="button"
tabIndex={0}
> >
<img <img
alt="Expand icon" alt="ChevronUpIcon"
src="" src=""
/> />
</div> </div>
@@ -175,13 +176,10 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleContainer" className="notificationConsoleContainer"
> >
<div <div
aria-expanded={false}
aria-label="Console"
className="notificationConsoleHeader" className="notificationConsoleHeader"
id="notificationConsoleHeader" id="notificationConsoleHeader"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
tabIndex={0} tabIndex={0}
> >
<div <div
@@ -194,7 +192,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="In progress items" alt="in progress items"
src={{}} src={{}}
/> />
<span <span
@@ -207,7 +205,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="Error items" alt="error items"
src={{}} src={{}}
/> />
<span <span
@@ -220,7 +218,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="Info items" alt="info items"
src={{}} src={{}}
/> />
<span <span
@@ -246,11 +244,15 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
</span> </span>
</div> </div>
<div <div
aria-expanded={true}
aria-label="console button collapsed"
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"
role="button"
tabIndex={0}
> >
<img <img
alt="Expand icon" alt="ChevronUpIcon"
src="" src=""
/> />
</div> </div>

View File

@@ -2,7 +2,7 @@
* Notebook container related stuff * Notebook container related stuff
*/ */
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import promiseRetry, { AbortError, Options } from "p-retry"; import promiseRetry, { AbortError } from "p-retry";
import { PhoenixClient } from "Phoenix/PhoenixClient"; import { PhoenixClient } from "Phoenix/PhoenixClient";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants"; import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
@@ -19,7 +19,7 @@ export class NotebookContainerClient {
private clearReconnectionAttemptMessage? = () => {}; private clearReconnectionAttemptMessage? = () => {};
private isResettingWorkspace: boolean; private isResettingWorkspace: boolean;
private phoenixClient: PhoenixClient; private phoenixClient: PhoenixClient;
private retryOptions: Options; private retryOptions: promiseRetry.Options;
private scheduleTimerId: NodeJS.Timeout; private scheduleTimerId: NodeJS.Timeout;
constructor(private onConnectionLost: () => void) { constructor(private onConnectionLost: () => void) {

View File

@@ -1,5 +1,6 @@
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { PhoenixClient } from "Phoenix/PhoenixClient"; import { PhoenixClient } from "Phoenix/PhoenixClient";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
@@ -127,7 +128,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
? databaseAccount?.location ? databaseAccount?.location
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase(); : databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri: string = `${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(); const authorizationHeader = getAuthorizationHeader();
try { try {
const response = await fetch(disallowedLocationsUri, { const response = await fetch(disallowedLocationsUri, {

View File

@@ -1,6 +1,6 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. // TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
import { configContext, Platform } from "ConfigContext";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import React from "react"; import React from "react";
import { ActionContracts } from "../../Contracts/ExplorerContracts"; import { ActionContracts } from "../../Contracts/ExplorerContracts";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@@ -58,9 +58,9 @@ function openCollectionTab(
} }
if ( if (
isFabricMirrored() && configContext.platform === Platform.Fabric &&
!( !(
// whitelist the tab kinds that are allowed to be opened in Fabric mirrored // whitelist the tab kinds that are allowed to be opened in Fabric
( (
action.tabKind === ActionContracts.TabKind.SQLDocuments || action.tabKind === ActionContracts.TabKind.SQLDocuments ||
action.tabKind === ActionContracts.TabKind.SQLQuery action.tabKind === ActionContracts.TabKind.SQLQuery

View File

@@ -1,6 +1,6 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import Explorer from "../../Explorer"; import Explorer from "../Explorer";
import { AddCollectionPanel } from "./AddCollectionPanel"; import { AddCollectionPanel } from "./AddCollectionPanel";
const props = { const props = {

View File

@@ -21,28 +21,13 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import { import {
AllPropertiesIndexed, FullTextPoliciesComponent,
AnalyticalStorageContent, getFullTextLanguageOptions,
ContainerVectorPolicyTooltipContent, } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
FullTextPolicyDefault, import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
getPartitionKey,
getPartitionKeyName,
getPartitionKeyPlaceHolder,
getPartitionKeyTooltipText,
isFreeTierAccount,
isSynapseLinkEnabled,
parseUniqueKeys,
scrollToSection,
SharedDatabaseDefault,
shouldShowAnalyticalStoreOptions,
UniqueKeysHeader,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react"; import React from "react";
import { CollectionCreation } from "Shared/Constants"; import { CollectionCreation } from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -56,15 +41,15 @@ import {
isVectorSearchEnabled, isVectorSearchEnabled,
} from "Utils/CapabilityUtils"; } from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils"; import { getUpsellMessage } from "Utils/PricingUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import "../Controls/ThroughputInput/ThroughputInput.less";
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator"; import { ContainerSampleGenerator } from "../DataSamples/ContainerSampleGenerator";
import Explorer from "../../Explorer"; import Explorer from "../Explorer";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../useDatabases";
import { PanelFooterComponent } from "../PanelFooterComponent"; import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen"; import { PanelLoadingScreen } from "./PanelLoadingScreen";
export interface AddCollectionPanelProps { export interface AddCollectionPanelProps {
explorer: Explorer; explorer: Explorer;
@@ -72,6 +57,40 @@ export interface AddCollectionPanelProps {
isQuickstart?: boolean; isQuickstart?: boolean;
} }
const SharedDatabaseDefault: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [],
excludedPaths: [
{
path: "/*",
},
],
};
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [
{
path: "/*",
indexes: [
{
kind: "Range",
dataType: "Number",
precision: -1,
},
{
kind: "Range",
dataType: "String",
precision: -1,
},
],
},
],
excludedPaths: [],
};
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = { export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
vectorEmbeddings: [], vectorEmbeddings: [],
}; };
@@ -124,7 +143,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "", collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
enableIndexing: true, enableIndexing: true,
isSharded: userContext.apiType !== "Tables", isSharded: userContext.apiType !== "Tables",
partitionKey: getPartitionKey(props.isQuickstart), partitionKey: this.getPartitionKey(),
subPartitionKeys: [], subPartitionKeys: [],
enableDedicatedThroughput: false, enableDedicatedThroughput: false,
createMongoWildCardIndex: createMongoWildCardIndex:
@@ -140,7 +159,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
vectorEmbeddingPolicy: [], vectorEmbeddingPolicy: [],
vectorIndexingPolicy: [], vectorIndexingPolicy: [],
vectorPolicyValidated: true, vectorPolicyValidated: true,
fullTextPolicy: FullTextPolicyDefault, fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
fullTextIndexes: [], fullTextIndexes: [],
fullTextPolicyValidated: true, fullTextPolicyValidated: true,
}; };
@@ -154,7 +173,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void { componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) { if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
scrollToSection("panelContainer"); this.scrollToSection("panelContainer");
} }
} }
@@ -171,7 +190,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/> />
)} )}
{!this.state.errorMessage && isFreeTierAccount() && ( {!this.state.errorMessage && this.isFreeTierAccount() && (
<PanelInfoErrorComponent <PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)} message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
messageType="info" messageType="info"
@@ -265,152 +284,150 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
<div className="panelMainContent"> <div className="panelMainContent">
{!(isFabricNative() && this.props.databaseId !== undefined) && ( <Stack hidden={userContext.apiType === "Tables"}>
<Stack hidden={userContext.apiType === "Tables"}> <Stack horizontal>
<Stack horizontal> <span className="mandatoryStar">*&nbsp;</span>
<span className="mandatoryStar">*&nbsp;</span> <Text className="panelTextBold" variant="small">
<Text className="panelTextBold" variant="small"> Database {userContext.apiType === "Mongo" ? "name" : "id"}
Database {userContext.apiType === "Mongo" ? "name" : "id"} </Text>
</Text> <TooltipHost
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge}
directionalHint={DirectionalHint.bottomLeftEdge} content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName( true,
).toLocaleLowerCase()}.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true, true,
).toLocaleLowerCase()}.`} ).toLocaleLowerCase()}.`}
> />
<Icon </TooltipHost>
iconName="Info" </Stack>
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
/>
</TooltipHost>
</Stack>
{configContext.platform !== Platform.Fabric && ( {configContext.platform !== Platform.Fabric && (
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<div role="radiogroup"> <div role="radiogroup">
<input
className="panelRadioBtn"
checked={this.state.createNewDatabase}
aria-label="Create new database"
aria-checked={this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
id="databaseCreateNew"
tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Create new</span>
<input
className="panelRadioBtn"
checked={!this.state.createNewDatabase}
aria-label="Use existing database"
aria-checked={!this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Use existing</span>
</div>
</Stack>
)}
{this.state.createNewDatabase && (
<Stack className="panelGroupSpacing">
<input <input
name="newDatabaseId" className="panelRadioBtn"
id="newDatabaseId" checked={this.state.createNewDatabase}
aria-required aria-label="Create new database"
required aria-checked={this.state.createNewDatabase}
type="text" name="databaseType"
autoComplete="off" type="radio"
pattern={ValidCosmosDbIdInputPattern.source} role="radio"
title={ValidCosmosDbIdDescription} id="databaseCreateNew"
placeholder="Type a new database id"
size={40}
className="panelTextField"
aria-label="New database id, Type a new database id"
autoFocus
tabIndex={0} tabIndex={0}
value={this.state.newDatabaseId} onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ newDatabaseId: event.target.value })
}
/> />
<span className="panelRadioBtnLabel">Create new</span>
{!isServerlessAccount() && ( <input
<Stack horizontal> className="panelRadioBtn"
<Checkbox checked={!this.state.createNewDatabase}
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`} aria-label="Use existing database"
checked={this.state.isSharedThroughputChecked} aria-checked={!this.state.createNewDatabase}
styles={{ name="databaseType"
text: { fontSize: 12 }, type="radio"
checkbox: { width: 12, height: 12 }, role="radio"
label: { padding: 0, alignItems: "center" }, tabIndex={0}
}} onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => />
this.setState({ isSharedThroughputChecked: isChecked }) <span className="panelRadioBtnLabel">Use existing</span>
} </div>
/> </Stack>
<TooltipHost )}
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName( {this.state.createNewDatabase && (
<Stack className="panelGroupSpacing">
<input
name="newDatabaseId"
id="newDatabaseId"
aria-required
required
type="text"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Type a new database id"
size={40}
className="panelTextField"
aria-label="New database id, Type a new database id"
autoFocus
tabIndex={0}
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ newDatabaseId: event.target.value })
}
/>
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ isSharedThroughputChecked: isChecked })
}
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
true, true,
).toLocaleLowerCase()} within the database.`} ).toLocaleLowerCase()} within the database.`}
> />
<Icon </TooltipHost>
iconName="Info" </Stack>
className="panelInfoIcon" )}
tabIndex={0}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
/>
</TooltipHost>
</Stack>
)}
{!isServerlessAccount() && this.state.isSharedThroughputChecked && ( {!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated} showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true} isDatabase={true}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
isFreeTier={isFreeTierAccount()} isFreeTier={this.isFreeTierAccount()}
isQuickstart={this.props.isQuickstart} isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)} setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded }) this.setState({ isThroughputCapExceeded })
} }
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)} onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
/> />
)} )}
</Stack> </Stack>
)} )}
{!this.state.createNewDatabase && ( {!this.state.createNewDatabase && (
<Dropdown <Dropdown
ariaLabel="Choose an existing database" ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }} styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }} style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database" placeholder="Choose an existing database"
options={this.getDatabaseOptions()} options={this.getDatabaseOptions()}
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) => onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
this.setState({ selectedDatabaseId: database.key as string }) this.setState({ selectedDatabaseId: database.key as string })
} }
defaultSelectedKey={this.props.databaseId} defaultSelectedKey={this.props.databaseId}
responsiveMode={999} responsiveMode={999}
/> />
)} )}
<Separator className="panelSeparator" /> <Separator className="panelSeparator" />
</Stack> </Stack>
)}
<Stack> <Stack>
<Stack horizontal> <Stack horizontal>
@@ -439,8 +456,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-required aria-required
required required
autoComplete="off" autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source} pattern="[^/?#\\]*[^/?# \\]"
title={ValidCosmosDbIdDescription} title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder={`e.g., ${getCollectionName()}1`} placeholder={`e.g., ${getCollectionName()}1`}
size={40} size={40}
className="panelTextField" className="panelTextField"
@@ -559,14 +576,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
{getPartitionKeyName()} {this.getPartitionKeyName()}
</Text> </Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}> <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getPartitionKeyTooltipText()}
>
<Icon <Icon
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel={getPartitionKeyTooltipText()} ariaLabel={this.getPartitionKeyTooltipText()}
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -580,8 +600,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
required required
size={40} size={40}
className="panelTextField" className="panelTextField"
placeholder={getPartitionKeyPlaceHolder()} placeholder={this.getPartitionKeyPlaceHolder()}
aria-label={getPartitionKeyName()} aria-label={this.getPartitionKeyName()}
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"} pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""} title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
value={this.state.partitionKey} value={this.state.partitionKey}
@@ -619,8 +639,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={index > 0 ? 1 : 0} tabIndex={index > 0 ? 1 : 0}
className="panelTextField" className="panelTextField"
autoComplete="off" autoComplete="off"
placeholder={getPartitionKeyPlaceHolder(index)} placeholder={this.getPartitionKeyPlaceHolder(index)}
aria-label={getPartitionKeyName()} aria-label={this.getPartitionKeyName()}
pattern={".*"} pattern={".*"}
title={""} title={""}
value={subPartitionKey} value={subPartitionKey}
@@ -646,7 +666,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
); );
})} })}
{!isFabricNative() && userContext.apiType === "SQL" && ( {userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<DefaultButton <DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }} styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
@@ -711,10 +731,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowCollectionThroughputInput() && ( {this.shouldShowCollectionThroughputInput() && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated} showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false} isDatabase={false}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
isFreeTier={isFreeTierAccount()} isFreeTier={this.isFreeTierAccount()}
isQuickstart={this.props.isQuickstart} isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)} setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
@@ -727,9 +747,29 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/> />
)} )}
{!isFabricNative() && userContext.apiType === "SQL" && ( {userContext.apiType === "SQL" && (
<Stack> <Stack>
{UniqueKeysHeader()} <Stack horizontal>
<Text className="panelTextBold" variant="small">
Unique keys
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
}
/>
</TooltipHost>
</Stack>
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => { {this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
return ( return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal> <Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
@@ -777,10 +817,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
)} )}
{shouldShowAnalyticalStoreOptions() && ( {this.shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
{AnalyticalStorageContent()} {this.getAnalyticalStorageContent()}
</Text> </Text>
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
@@ -788,7 +828,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={this.state.enableAnalyticalStore} checked={this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()} disabled={!this.isSynapseLinkEnabled()}
aria-label="Enable analytical store" aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore} aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
@@ -803,7 +843,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore} checked={!this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()} disabled={!this.isSynapseLinkEnabled()}
aria-label="Disable analytical store" aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore} aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
@@ -817,7 +857,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div> </div>
</Stack> </Stack>
{!isSynapseLinkEnabled() && ( {!this.isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Text variant="small"> <Text variant="small">
Azure Synapse Link is required for creating an analytical store{" "} Azure Synapse Link is required for creating an analytical store{" "}
@@ -825,7 +865,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Link <Link
href="https://aka.ms/cosmosdb-synapselink" href="https://aka.ms/cosmosdb-synapselink"
target="_blank" target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link" className="capacitycalculator-link"
> >
Learn more Learn more
@@ -847,9 +886,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
title="Container Vector Policy" title="Container Vector Policy"
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
scrollToSection("collapsibleVectorPolicySectionContent"); this.scrollToSection("collapsibleVectorPolicySectionContent");
}} }}
tooltipContent={ContainerVectorPolicyTooltipContent()} tooltipContent={this.getContainerVectorPolicyTooltipContent()}
> >
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}> <Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}> <Stack styles={{ root: { paddingLeft: 40 } }}>
@@ -875,7 +914,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
title="Container Full Text Search Policy" title="Container Full Text Search Policy"
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent"); this.scrollToSection("collapsibleFullTextPolicySectionContent");
}} }}
//TODO: uncomment when learn more text becomes available //TODO: uncomment when learn more text becomes available
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()} // tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
@@ -897,13 +936,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</CollapsibleSectionComponent> </CollapsibleSectionComponent>
</Stack> </Stack>
)} )}
{!isFabricNative() && userContext.apiType !== "Tables" && ( {userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent <CollapsibleSectionComponent
title="Advanced" title="Advanced"
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
scrollToSection("collapsibleAdvancedSectionContent"); this.scrollToSection("collapsibleAdvancedSectionContent");
}} }}
> >
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent"> <Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
@@ -1013,6 +1052,31 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
})); }));
} }
private getPartitionKeyName(isLowerCase?: boolean): string {
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}
private getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) {
case "Mongo":
return "e.g., categoryId";
case "Gremlin":
return "e.g., /address";
case "SQL":
return `${
index === undefined
? "Required - first partition key e.g., /TenantId"
: index === 0
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return "e.g., /address/zipCode";
}
}
private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void { private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && !this.state.createNewDatabase) { if (event.target.checked && !this.state.createNewDatabase) {
this.setState({ this.setState({
@@ -1100,12 +1164,48 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return !!selectedDatabase?.offer(); return !!selectedDatabase?.offer();
} }
private isFreeTierAccount(): boolean {
return userContext.databaseAccount?.properties?.enableFreeTier;
}
private getFreeTierIndexingText(): string { private getFreeTierIndexingText(): string {
return this.state.enableIndexing return this.state.enableIndexing
? "All properties in your documents will be indexed by default for flexible and efficient queries." ? "All properties in your documents will be indexed by default for flexible and efficient queries."
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations."; : "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
} }
private getPartitionKeyTooltipText(): string {
if (userContext.apiType === "Mongo") {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = `The ${this.getPartitionKeyName(
true,
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
if (userContext.apiType === "SQL") {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
}
private getPartitionKey(): string {
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
return "";
}
if (userContext.features.partitionKeyDefault) {
return userContext.apiType === "SQL" ? "/id" : "_id";
}
if (userContext.features.partitionKeyDefault2) {
return userContext.apiType === "SQL" ? "/pk" : "pk";
}
if (this.props.isQuickstart) {
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
}
return "";
}
private getPartitionKeySubtext(): string { private getPartitionKeySubtext(): string {
if ( if (
userContext.features.partitionKeyDefault && userContext.features.partitionKeyDefault &&
@@ -1117,6 +1217,30 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return ""; return "";
} }
private getAnalyticalStorageContent(): 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 target="_blank" href="https://aka.ms/analytical-store-overview">
Learn more
</Link>
</Text>
);
}
private getContainerVectorPolicyTooltipContent(): JSX.Element {
return (
<Text variant="small">
Describe any properties in your data that contain vectors, so that they can be made available for similarity
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
Learn more
</Link>
</Text>
);
}
//TODO: uncomment when learn more text becomes available //TODO: uncomment when learn more text becomes available
// private getContainerFullTextPolicyTooltipContent(): JSX.Element { // private getContainerFullTextPolicyTooltipContent(): JSX.Element {
// return ( // return (
@@ -1131,7 +1255,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// } // }
private shouldShowCollectionThroughputInput(): boolean { private shouldShowCollectionThroughputInput(): boolean {
if (isFabricNative() || isServerlessAccount()) { if (isServerlessAccount()) {
return false; return false;
} }
@@ -1147,7 +1271,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private shouldShowIndexingOptionsForFreeTierAccount(): boolean { private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
if (!isFreeTierAccount()) { if (!this.isFreeTierAccount()) {
return false; return false;
} }
@@ -1156,6 +1280,39 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: this.isSelectedDatabaseSharedThroughput(); : this.isSelectedDatabaseSharedThroughput();
} }
private shouldShowAnalyticalStoreOptions(): boolean {
if (configContext.platform === Platform.Emulator) {
return false;
}
switch (userContext.apiType) {
case "SQL":
case "Mongo":
return true;
default:
return false;
}
}
private isSynapseLinkEnabled(): boolean {
if (!userContext.databaseAccount) {
return false;
}
const { properties } = userContext.databaseAccount;
if (!properties) {
return false;
}
if (properties.enableAnalyticalStorage) {
return true;
}
return properties.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
);
}
private shouldShowVectorSearchParameters() { private shouldShowVectorSearchParameters() {
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput()); return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
} }
@@ -1236,11 +1393,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private getAnalyticalStorageTtl(): number { private getAnalyticalStorageTtl(): number {
if (!isSynapseLinkEnabled()) { if (!this.isSynapseLinkEnabled()) {
return undefined; return undefined;
} }
if (!shouldShowAnalyticalStoreOptions()) { if (!this.shouldShowAnalyticalStoreOptions()) {
return undefined; return undefined;
} }
@@ -1254,6 +1411,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return Constants.AnalyticalStorageTtl.Disabled; return Constants.AnalyticalStorageTtl.Disabled;
} }
private scrollToSection(id: string): void {
document.getElementById(id)?.scrollIntoView();
}
private getSampleDBName(): string { private getSampleDBName(): string {
const existingSampleDBs = useDatabases const existingSampleDBs = useDatabases
.getState() .getState()
@@ -1288,7 +1449,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
partitionKeyString = "/'$pk'"; partitionKeyString = "/'$pk'";
} }
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys); const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2; const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
const partitionKey: DataModels.PartitionKey = partitionKeyString const partitionKey: DataModels.PartitionKey = partitionKeyString
? { ? {

View File

@@ -1,217 +0,0 @@
import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react";
import { userContext } from "UserContext";
export function getPartitionKeyTooltipText(): string {
if (userContext.apiType === "Mongo") {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = `The ${getPartitionKeyName(
true,
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
if (userContext.apiType === "SQL") {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
}
export function getPartitionKeyName(isLowerCase?: boolean): string {
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}
export function getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) {
case "Mongo":
return "e.g., categoryId";
case "Gremlin":
return "e.g., /address";
case "SQL":
return `${
index === undefined
? "Required - first partition key e.g., /TenantId"
: index === 0
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return "e.g., /address/zipCode";
}
}
export function getPartitionKey(isQuickstart?: boolean): string {
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
return "";
}
if (userContext.features.partitionKeyDefault) {
return userContext.apiType === "SQL" ? "/id" : "_id";
}
if (userContext.features.partitionKeyDefault2) {
return userContext.apiType === "SQL" ? "/pk" : "pk";
}
if (isQuickstart) {
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
}
return "";
}
export function isFreeTierAccount(): boolean {
return userContext.databaseAccount?.properties?.enableFreeTier;
}
export function UniqueKeysHeader(): JSX.Element {
const tooltipContent =
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.";
return (
<Stack horizontal>
<Text className="panelTextBold" variant="small">
Unique keys
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
</TooltipHost>
</Stack>
);
}
export function shouldShowAnalyticalStoreOptions(): boolean {
if (isFabricNative() || configContext.platform === Platform.Emulator) {
return false;
}
switch (userContext.apiType) {
case "SQL":
case "Mongo":
return true;
default:
return false;
}
}
export function AnalyticalStorageContent(): JSX.Element {
return (
<Text variant="small">
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
the performance of transactional workloads.{" "}
<Link
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank"
href="https://aka.ms/analytical-store-overview"
>
Learn more
</Link>
</Text>
);
}
export function isSynapseLinkEnabled(): boolean {
if (!userContext.databaseAccount) {
return false;
}
const { properties } = userContext.databaseAccount;
if (!properties) {
return false;
}
if (properties.enableAnalyticalStorage) {
return true;
}
return properties.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
);
}
export function scrollToSection(id: string): void {
document.getElementById(id)?.scrollIntoView();
}
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
return (
<Text variant="small">
Describe any properties in your data that contain vectors, so that they can be made available for similarity
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
Learn more
</Link>
</Text>
);
}
export function parseUniqueKeys(uniqueKeys: string[]): DataModels.UniqueKeyPolicy {
if (uniqueKeys?.length === 0) {
return undefined;
}
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = { uniqueKeys: [] };
uniqueKeys.forEach((uniqueKey: string) => {
if (uniqueKey) {
const validPaths: string[] = uniqueKey.split(",")?.filter((path) => path?.length > 0);
const trimmedPaths: string[] = validPaths?.map((path) => path.trim());
if (trimmedPaths?.length > 0) {
if (userContext.apiType === "Mongo") {
trimmedPaths.map((path) => {
const transformedPath = path.split(".").join("/");
if (transformedPath[0] !== "/") {
return "/" + transformedPath;
}
return transformedPath;
});
}
uniqueKeyPolicy.uniqueKeys.push({ paths: trimmedPaths });
}
}
});
return uniqueKeyPolicy;
}
export const SharedDatabaseDefault: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [],
excludedPaths: [
{
path: "/*",
},
],
};
export const FullTextPolicyDefault: DataModels.FullTextPolicy = {
defaultLanguage: getFullTextLanguageOptions()[0].key as never,
fullTextPaths: [],
};
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [
{
path: "/*",
indexes: [
{
kind: "Range",
dataType: "Number",
precision: -1,
},
{
kind: "Range",
dataType: "String",
precision: -1,
},
],
},
],
excludedPaths: [],
};

View File

@@ -1,6 +1,5 @@
import { Checkbox, Stack, Text, TextField } from "@fluentui/react"; import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility"; import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
@@ -205,8 +204,8 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
type="text" type="text"
aria-required="true" aria-required="true"
autoComplete="off" autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source} pattern="[^/?#\\]*[^/?# \\]"
title={ValidCosmosDbIdDescription} title="May not end with space nor contain characters '\' '/' '#' '?'"
size={40} size={40}
aria-label={databaseIdLabel} aria-label={databaseIdLabel}
placeholder={databaseIdPlaceHolder} placeholder={databaseIdPlaceHolder}

View File

@@ -39,7 +39,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
data-lpignore={true} data-lpignore={true}
id="database-id" id="database-id"
onChange={[Function]} onChange={[Function]}
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]" pattern="[^/?#\\\\]*[^/?# \\\\]"
placeholder="Type a new database id" placeholder="Type a new database id"
size={40} size={40}
styles={ styles={

View File

@@ -1,28 +0,0 @@
import { shallow, ShallowWrapper } from "enzyme";
import Explorer from "Explorer/Explorer";
import {
AddGlobalSecondaryIndexPanel,
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import React, { Component } from "react";
const props: AddGlobalSecondaryIndexPanelProps = {
explorer: new Explorer(),
};
describe("AddGlobalSecondaryIndexPanel", () => {
it("render default panel", () => {
const wrapper: ShallowWrapper<AddGlobalSecondaryIndexPanelProps, object, Component> = shallow(
<AddGlobalSecondaryIndexPanel {...props} />,
);
expect(wrapper).toMatchSnapshot();
});
it("should render form", () => {
const wrapper: ShallowWrapper<AddGlobalSecondaryIndexPanelProps, object, Component> = shallow(
<AddGlobalSecondaryIndexPanel {...props} />,
);
const form = wrapper.find("form").first();
expect(form).toBeDefined();
});
});

View File

@@ -1,431 +0,0 @@
import {
DirectionalHint,
Dropdown,
DropdownMenuItemType,
Icon,
IDropdownOption,
Link,
Separator,
Stack,
Text,
TooltipHost,
} from "@fluentui/react";
import * as Constants from "Common/Constants";
import { createGlobalSecondaryIndex } from "Common/dataAccess/createMaterializedView";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import * as DataModels from "Contracts/DataModels";
import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { Collection, Database } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import {
AllPropertiesIndexed,
FullTextPolicyDefault,
getPartitionKey,
isSynapseLinkEnabled,
parseUniqueKeys,
scrollToSection,
shouldShowAnalyticalStoreOptions,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import {
chooseSourceContainerStyle,
chooseSourceContainerStyles,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles";
import { AdvancedComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent";
import { AnalyticalStoreComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent";
import { FullTextSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent";
import { PartitionKeyComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent";
import { ThroughputComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent";
import { UniqueKeysComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent";
import { VectorSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent";
import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent";
import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent";
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
import { useDatabases } from "Explorer/useDatabases";
import { useSidePanel } from "hooks/useSidePanel";
import React, { useEffect, useState } from "react";
import { CollectionCreation } from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
export interface AddGlobalSecondaryIndexPanelProps {
explorer: Explorer;
sourceContainer?: Collection;
}
export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanelProps): JSX.Element => {
const { explorer, sourceContainer } = props;
const [sourceContainerOptions, setSourceContainerOptions] = useState<IDropdownOption[]>();
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
const [globalSecondaryIndexId, setGlobalSecondaryIndexId] = useState<string>();
const [definition, setDefinition] = useState<string>();
const [partitionKey, setPartitionKey] = useState<string>(getPartitionKey());
const [subPartitionKeys, setSubPartitionKeys] = useState<string[]>([]);
const [useHashV1, setUseHashV1] = useState<boolean>();
const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState<boolean>();
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>();
const [uniqueKeys, setUniqueKeys] = useState<string[]>([]);
const [enableAnalyticalStore, setEnableAnalyticalStore] = useState<boolean>();
const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState<VectorEmbedding[]>();
const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState<VectorIndex[]>();
const [vectorPolicyValidated, setVectorPolicyValidated] = useState<boolean>();
const [fullTextPolicy, setFullTextPolicy] = useState<FullTextPolicy>(FullTextPolicyDefault);
const [fullTextIndexes, setFullTextIndexes] = useState<FullTextIndex[]>();
const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState<boolean>();
const [errorMessage, setErrorMessage] = useState<string>();
const [showErrorDetails, setShowErrorDetails] = useState<boolean>();
const [isExecuting, setIsExecuting] = useState<boolean>();
useEffect(() => {
const sourceContainerOptions: IDropdownOption[] = [];
useDatabases.getState().databases.forEach((database: Database) => {
sourceContainerOptions.push({
key: database.rid,
text: database.id(),
itemType: DropdownMenuItemType.Header,
});
database.collections().forEach((collection: Collection) => {
const isGlobalSecondaryIndex: boolean = !!collection.materializedViewDefinition();
sourceContainerOptions.push({
key: collection.rid,
text: collection.id(),
disabled: isGlobalSecondaryIndex,
...(isGlobalSecondaryIndex && {
title: "This is a global secondary index.",
}),
data: collection,
});
});
});
setSourceContainerOptions(sourceContainerOptions);
}, []);
useEffect(() => {
scrollToSection("panelContainer");
}, [errorMessage]);
let globalSecondaryIndexThroughput: number;
let isGlobalSecondaryIndexAutoscale: boolean;
let isCostAcknowledged: boolean;
const globalSecondaryIndexThroughputOnChange = (globalSecondaryIndexThroughputValue: number): void => {
globalSecondaryIndexThroughput = globalSecondaryIndexThroughputValue;
};
const isGlobalSecondaryIndexAutoscaleOnChange = (isGlobalSecondaryIndexAutoscaleValue: boolean): void => {
isGlobalSecondaryIndexAutoscale = isGlobalSecondaryIndexAutoscaleValue;
};
const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => {
isCostAcknowledged = isCostAcknowledgedValue;
};
const isSelectedSourceContainerSharedThroughput = (): boolean => {
if (!selectedSourceContainer) {
return false;
}
return !!selectedSourceContainer.getDatabase().offer();
};
const showCollectionThroughputInput = (): boolean => {
if (isServerlessAccount()) {
return false;
}
if (enableDedicatedThroughput) {
return true;
}
return !!selectedSourceContainer && !isSelectedSourceContainerSharedThroughput();
};
const showVectorSearchParameters = (): boolean => {
return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
};
const showFullTextSearchParameters = (): boolean => {
return isFullTextSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
};
const getAnalyticalStorageTtl = (): number => {
if (!isSynapseLinkEnabled()) {
return undefined;
}
if (!shouldShowAnalyticalStoreOptions()) {
return undefined;
}
if (enableAnalyticalStore) {
// TODO: always default to 90 days once the backend hotfix is deployed
return userContext.features.ttl90Days
? Constants.AnalyticalStorageTtl.Days90
: Constants.AnalyticalStorageTtl.Infinite;
}
return Constants.AnalyticalStorageTtl.Disabled;
};
const validateInputs = (): boolean => {
if (!selectedSourceContainer) {
setErrorMessage("Please select a source container");
return false;
}
if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage = isGlobalSecondaryIndexAutoscale
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
setErrorMessage(errorMessage);
return false;
}
if (globalSecondaryIndexThroughput > CollectionCreation.MaxRUPerPartition) {
setErrorMessage("Unsharded collections support up to 10,000 RUs");
return false;
}
if (showVectorSearchParameters()) {
if (!vectorPolicyValidated) {
setErrorMessage("Please fix errors in container vector policy");
return false;
}
if (!fullTextPolicyValidated) {
setErrorMessage("Please fix errors in container full text search policy");
return false;
}
}
return true;
};
const submit = async (event?: React.FormEvent<HTMLFormElement>): Promise<void> => {
event?.preventDefault();
if (!validateInputs()) {
return;
}
const globalSecondaryIdTrimmed: string = globalSecondaryIndexId.trim();
const globalSecondaryIndexDefinition: DataModels.MaterializedViewDefinition = {
sourceCollectionId: selectedSourceContainer.id(),
definition: definition,
};
const partitionKeyTrimmed: string = partitionKey.trim();
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(uniqueKeys);
const partitionKeyVersion = useHashV1 ? undefined : 2;
const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed
? {
paths: [
partitionKeyTrimmed,
...(userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? subPartitionKeys : []),
],
kind: userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
version: partitionKeyVersion,
}
: undefined;
const indexingPolicy: DataModels.IndexingPolicy = AllPropertiesIndexed;
let vectorEmbeddingPolicyFinal: DataModels.VectorEmbeddingPolicy;
if (showVectorSearchParameters()) {
indexingPolicy.vectorIndexes = vectorIndexingPolicy;
vectorEmbeddingPolicyFinal = {
vectorEmbeddings: vectorEmbeddingPolicy,
};
}
if (showFullTextSearchParameters()) {
indexingPolicy.fullTextIndexes = fullTextIndexes;
}
const telemetryData: TelemetryProcessor.TelemetryData = {
database: {
id: selectedSourceContainer.databaseId,
shared: isSelectedSourceContainerSharedThroughput(),
},
collection: {
id: globalSecondaryIdTrimmed,
throughput: globalSecondaryIndexThroughput,
isAutoscale: isGlobalSecondaryIndexAutoscale,
partitionKeyPaths,
uniqueKeyPolicy,
collectionWithDedicatedThroughput: enableDedicatedThroughput,
},
subscriptionQuotaId: userContext.quotaId,
dataExplorerArea: Constants.Areas.ContextualPane,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData);
const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput;
let offerThroughput: number;
let autoPilotMaxThroughput: number;
if (!databaseLevelThroughput) {
if (isGlobalSecondaryIndexAutoscale) {
autoPilotMaxThroughput = globalSecondaryIndexThroughput;
} else {
offerThroughput = globalSecondaryIndexThroughput;
}
}
const createGlobalSecondaryIndexParams: DataModels.CreateMaterializedViewsParams = {
materializedViewId: globalSecondaryIdTrimmed,
materializedViewDefinition: globalSecondaryIndexDefinition,
databaseId: selectedSourceContainer.databaseId,
databaseLevelThroughput: databaseLevelThroughput,
offerThroughput: offerThroughput,
autoPilotMaxThroughput: autoPilotMaxThroughput,
analyticalStorageTtl: getAnalyticalStorageTtl(),
indexingPolicy: indexingPolicy,
partitionKey: partitionKeyPaths,
uniqueKeyPolicy: uniqueKeyPolicy,
vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal,
fullTextPolicy: fullTextPolicy,
};
setIsExecuting(true);
try {
await createGlobalSecondaryIndex(createGlobalSecondaryIndexParams);
await explorer.refreshAllDatabases();
TelemetryProcessor.traceSuccess(Action.CreateGlobalSecondaryIndex, telemetryData, startKey);
useSidePanel.getState().closeSidePanel();
} catch (error) {
const errorMessage: string = getErrorMessage(error);
setErrorMessage(errorMessage);
setShowErrorDetails(true);
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
TelemetryProcessor.traceFailure(Action.CreateGlobalSecondaryIndex, failureTelemetryData, startKey);
} finally {
setIsExecuting(false);
}
};
return (
<form className="panelFormWrapper" id="panelGlobalSecondaryIndex" onSubmit={submit}>
{errorMessage && (
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
)}
<div className="panelMainContent">
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Source container id
</Text>
</Stack>
<Dropdown
placeholder="Choose source container"
options={sourceContainerOptions}
defaultSelectedKey={selectedSourceContainer?.rid}
styles={chooseSourceContainerStyles()}
style={chooseSourceContainerStyle()}
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
/>
<Separator className="panelSeparator" />
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Global secondary index container id
</Text>
</Stack>
<input
id="globalSecondaryIndexId"
type="text"
aria-required
required
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={`e.g., indexbyEmailId`}
size={40}
className="panelTextField"
value={globalSecondaryIndexId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setGlobalSecondaryIndexId(event.target.value)}
/>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Global secondary index definition
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
target="blank"
>
Learn more about defining global secondary indexes.
</Link>
}
>
<Icon role="button" iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
</Stack>
<input
id="globalSecondaryIndexDefinition"
type="text"
aria-required
required
autoComplete="off"
placeholder={"SELECT c.email, c.accountId FROM c"}
size={40}
className="panelTextField"
value={definition || ""}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDefinition(event.target.value)}
/>
<PartitionKeyComponent
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
/>
<ThroughputComponent
{...{
enableDedicatedThroughput,
setEnabledDedicatedThroughput,
isSelectedSourceContainerSharedThroughput,
showCollectionThroughputInput,
globalSecondaryIndexThroughputOnChange,
isGlobalSecondaryIndexAutoscaleOnChange,
setIsThroughputCapExceeded,
isCostAknowledgedOnChange,
}}
/>
<UniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
{shouldShowAnalyticalStoreOptions() && (
<AnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
)}
{showVectorSearchParameters() && (
<VectorSearchComponent
{...{
vectorEmbeddingPolicy,
setVectorEmbeddingPolicy,
vectorIndexingPolicy,
setVectorIndexingPolicy,
vectorPolicyValidated,
setVectorPolicyValidated,
}}
/>
)}
{showFullTextSearchParameters() && (
<FullTextSearchComponent
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
/>
)}
<AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
</Stack>
</div>
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
{isExecuting && <PanelLoadingScreen />}
</form>
);
};

View File

@@ -1,15 +0,0 @@
import { IDropdownStyleProps, IDropdownStyles, IStyleFunctionOrObject } from "@fluentui/react";
import { CSSProperties } from "react";
export function chooseSourceContainerStyles(): IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> {
return {
title: { height: 27, lineHeight: 27 },
dropdownItem: { fontSize: 12 },
dropdownItemDisabled: { fontSize: 12 },
dropdownItemSelected: { fontSize: 12 },
};
}
export function chooseSourceContainerStyle(): CSSProperties {
return { width: 300, fontSize: 12 };
}

View File

@@ -1,54 +0,0 @@
import { Checkbox, Icon, Link, Stack, Text } from "@fluentui/react";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
export interface AdvancedComponentProps {
useHashV1: boolean;
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
}
export const AdvancedComponent = (props: AdvancedComponentProps): JSX.Element => {
const { useHashV1, setUseHashV1, setSubPartitionKeys } = props;
const useHashV1CheckboxOnChange = (isChecked: boolean): void => {
setUseHashV1(isChecked);
setSubPartitionKeys([]);
};
return (
<CollapsibleSectionComponent
title="Advanced"
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddGlobalSecondaryIndexPaneAdvancedSection);
scrollToSection("collapsibleAdvancedSectionContent");
}}
>
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
<Checkbox
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
checked={useHashV1}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
useHashV1CheckboxOnChange(isChecked);
}}
/>
<Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the created
container will use a legacy partitioning scheme that supports partition key values of size only up to 101
bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
Learn more
</Link>
</Text>
</Stack>
</CollapsibleSectionComponent>
);
};

View File

@@ -1,99 +0,0 @@
import { DefaultButton, Link, Stack, Text } from "@fluentui/react";
import * as Constants from "Common/Constants";
import Explorer from "Explorer/Explorer";
import {
AnalyticalStorageContent,
isSynapseLinkEnabled,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { getCollectionName } from "Utils/APITypeUtils";
export interface AnalyticalStoreComponentProps {
explorer: Explorer;
enableAnalyticalStore: boolean;
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
}
export const AnalyticalStoreComponent = (props: AnalyticalStoreComponentProps): JSX.Element => {
const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props;
const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => {
if (checked && !enableAnalyticalStore) {
setEnableAnalyticalStore(true);
}
};
const onDisableAnalyticalStoreRadioButtonnChange = (checked: boolean): void => {
if (checked && enableAnalyticalStore) {
setEnableAnalyticalStore(false);
}
};
return (
<Stack className="panelGroupSpacing">
<Text className="panelTextBold" variant="small">
{AnalyticalStorageContent()}
</Text>
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="enableAnalyticalStoreBtn"
tabIndex={0}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onEnableAnalyticalStoreRadioButtonChange(event.target.checked);
}}
/>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="disableAnalyticalStoreBtn"
tabIndex={0}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onDisableAnalyticalStoreRadioButtonnChange(event.target.checked);
}}
/>
<span className="panelRadioBtnLabel">Off</span>
</div>
</Stack>
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small">
Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}.
Enable Synapse Link for this Cosmos DB account.{" "}
<Link
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link"
>
Learn more
</Link>
</Text>
<DefaultButton
text="Enable"
onClick={() => explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
/>
</Stack>
)}
</Stack>
);
};

View File

@@ -1,45 +0,0 @@
import { Stack } from "@fluentui/react";
import { FullTextIndex, FullTextPolicy } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
export interface FullTextSearchComponentProps {
fullTextPolicy: FullTextPolicy;
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
}
export const FullTextSearchComponent = (props: FullTextSearchComponentProps): JSX.Element => {
const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props;
return (
<Stack>
<CollapsibleSectionComponent
title="Container Full Text Search Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent");
}}
>
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
<FullTextPoliciesComponent
fullTextPolicy={fullTextPolicy}
onFullTextPathChange={(
fullTextPolicy: FullTextPolicy,
fullTextIndexes: FullTextIndex[],
fullTextPolicyValidated: boolean,
) => {
setFullTextPolicy(fullTextPolicy);
setFullTextIndexes(fullTextIndexes);
setFullTextPolicyValidated(fullTextPolicyValidated);
}}
/>
</Stack>
</Stack>
</CollapsibleSectionComponent>
</Stack>
);
};

View File

@@ -1,132 +0,0 @@
import { DefaultButton, DirectionalHint, Icon, IconButton, Link, Stack, Text, TooltipHost } from "@fluentui/react";
import * as Constants from "Common/Constants";
import {
getPartitionKeyName,
getPartitionKeyPlaceHolder,
getPartitionKeyTooltipText,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
export interface PartitionKeyComponentProps {
partitionKey?: string;
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
subPartitionKeys: string[];
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
useHashV1: boolean;
}
export const PartitionKeyComponent = (props: PartitionKeyComponentProps): JSX.Element => {
const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props;
const partitionKeyValueOnChange = (value: string): void => {
if (!partitionKey && !value.startsWith("/")) {
setPartitionKey("/" + value);
} else {
setPartitionKey(value);
}
};
const subPartitionKeysValueOnChange = (value: string, index: number): void => {
const updatedSubPartitionKeys: string[] = [...subPartitionKeys];
if (!updatedSubPartitionKeys[index] && !value.startsWith("/")) {
updatedSubPartitionKeys[index] = "/" + value.trim();
} else {
updatedSubPartitionKeys[index] = value.trim();
}
setSubPartitionKeys(updatedSubPartitionKeys);
};
return (
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Partition key
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
</Stack>
<input
type="text"
id="addGlobalSecondaryIndex-partitionKeyValue"
aria-required
required
size={40}
className="panelTextField"
placeholder={getPartitionKeyPlaceHolder()}
aria-label={getPartitionKeyName()}
pattern=".*"
value={partitionKey}
style={{ marginBottom: 8 }}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
partitionKeyValueOnChange(event.target.value);
}}
/>
{subPartitionKeys.map((subPartitionKey: string, subPartitionKeyIndex: number) => {
return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${subPartitionKeyIndex}`} horizontal>
<div
style={{
width: "20px",
border: "solid",
borderWidth: "0px 0px 1px 1px",
marginRight: "5px",
}}
></div>
<input
type="text"
id="addGlobalSecondaryIndex-partitionKeyValue"
key={`addGlobalSecondaryIndex-partitionKeyValue_${subPartitionKeyIndex}`}
aria-required
required
size={40}
tabIndex={subPartitionKeyIndex > 0 ? 1 : 0}
className="panelTextField"
autoComplete="off"
placeholder={getPartitionKeyPlaceHolder(subPartitionKeyIndex)}
aria-label={getPartitionKeyName()}
pattern={".*"}
title={""}
value={subPartitionKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
subPartitionKeysValueOnChange(event.target.value, subPartitionKeyIndex);
}}
/>
<IconButton
iconProps={{ iconName: "Delete" }}
style={{ height: 27 }}
onClick={() => {
const updatedSubPartitionKeys = subPartitionKeys.filter(
(_, subPartitionKeyIndexToRemove) => subPartitionKeyIndex !== subPartitionKeyIndexToRemove,
);
setSubPartitionKeys(updatedSubPartitionKeys);
}}
/>
</Stack>
);
})}
<Stack className="panelGroupSpacing">
<DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
hidden={useHashV1}
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
>
Add hierarchical partition key
</DefaultButton>
{subPartitionKeys.length > 0 && (
<Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to partition your
data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview
JavaScript V3 SDK.{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
Learn more
</Link>
</Text>
)}
</Stack>
</Stack>
);
};

View File

@@ -1,71 +0,0 @@
import { Checkbox, Stack } from "@fluentui/react";
import { ThroughputInput } from "Explorer/Controls/ThroughputInput/ThroughputInput";
import { isFreeTierAccount } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useDatabases } from "Explorer/useDatabases";
import React from "react";
import { getCollectionName } from "Utils/APITypeUtils";
import { isServerlessAccount } from "Utils/CapabilityUtils";
export interface ThroughputComponentProps {
enableDedicatedThroughput: boolean;
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
isSelectedSourceContainerSharedThroughput: () => boolean;
showCollectionThroughputInput: () => boolean;
globalSecondaryIndexThroughputOnChange: (globalSecondaryIndexThroughputValue: number) => void;
isGlobalSecondaryIndexAutoscaleOnChange: (isGlobalSecondaryIndexAutoscaleValue: boolean) => void;
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
}
export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Element => {
const {
enableDedicatedThroughput,
setEnabledDedicatedThroughput,
isSelectedSourceContainerSharedThroughput,
showCollectionThroughputInput,
globalSecondaryIndexThroughputOnChange,
isGlobalSecondaryIndexAutoscaleOnChange,
setIsThroughputCapExceeded,
isCostAknowledgedOnChange,
} = props;
return (
<Stack>
{!isServerlessAccount() && isSelectedSourceContainerSharedThroughput() && (
<Stack horizontal verticalAlign="center">
<Checkbox
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
checked={enableDedicatedThroughput}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(_, isChecked: boolean) => setEnabledDedicatedThroughput(isChecked)}
/>
</Stack>
)}
{showCollectionThroughputInput() && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false}
isSharded={false}
isFreeTier={isFreeTierAccount()}
isQuickstart={false}
setThroughputValue={(throughput: number) => {
globalSecondaryIndexThroughputOnChange(throughput);
}}
setIsAutoscale={(isAutoscale: boolean) => {
isGlobalSecondaryIndexAutoscaleOnChange(isAutoscale);
}}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
setIsThroughputCapExceeded(isThroughputCapExceeded);
}}
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
isCostAknowledgedOnChange(isAcknowledged);
}}
/>
)}
</Stack>
);
};

View File

@@ -1,78 +0,0 @@
import { ActionButton, IconButton, Stack } from "@fluentui/react";
import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { userContext } from "UserContext";
export interface UniqueKeysComponentProps {
uniqueKeys: string[];
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
}
export const UniqueKeysComponent = (props: UniqueKeysComponentProps): JSX.Element => {
const { uniqueKeys, setUniqueKeys } = props;
const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => {
const updatedUniqueKeys = uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number) => {
if (uniqueKeyToReplaceIndex === uniqueKeyIndex) {
return value;
}
return uniqueKey;
});
setUniqueKeys(updatedUniqueKeys);
};
const deleteUniqueKeyOnClick = (uniqueKeyToDeleteIndex: number): void => {
const updatedUniqueKeys = uniqueKeys.filter((_, uniqueKeyIndex) => uniqueKeyToDeleteIndex !== uniqueKeyIndex);
setUniqueKeys(updatedUniqueKeys);
};
const addUniqueKeyOnClick = (): void => {
setUniqueKeys([...uniqueKeys, ""]);
};
return (
<Stack>
{UniqueKeysHeader()}
{uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number): JSX.Element => {
return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey-${uniqueKeyIndex}`} horizontal>
<input
type="text"
autoComplete="off"
placeholder={
userContext.apiType === "Mongo"
? "Comma separated paths e.g. firstName,address.zipCode"
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className="panelTextField"
autoFocus
value={uniqueKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
updateUniqueKeysOnChange(event.target.value, uniqueKeyIndex);
}}
/>
<IconButton
iconProps={{ iconName: "Delete" }}
style={{ height: 27 }}
onClick={() => {
deleteUniqueKeyOnClick(uniqueKeyIndex);
}}
/>
</Stack>
);
})}
<ActionButton
iconProps={{ iconName: "Add" }}
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
onClick={() => {
addUniqueKeyOnClick();
}}
>
Add unique key
</ActionButton>
</Stack>
);
};

View File

@@ -1,58 +0,0 @@
import { Stack } from "@fluentui/react";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import {
ContainerVectorPolicyTooltipContent,
scrollToSection,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
export interface VectorSearchComponentProps {
vectorEmbeddingPolicy: VectorEmbedding[];
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
vectorIndexingPolicy: VectorIndex[];
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
}
export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.Element => {
const {
vectorEmbeddingPolicy,
setVectorEmbeddingPolicy,
vectorIndexingPolicy,
setVectorIndexingPolicy,
setVectorPolicyValidated,
} = props;
return (
<Stack>
<CollapsibleSectionComponent
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleVectorPolicySectionContent");
}}
tooltipContent={ContainerVectorPolicyTooltipContent()}
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
<VectorEmbeddingPoliciesComponent
vectorEmbeddings={vectorEmbeddingPolicy}
vectorIndexes={vectorIndexingPolicy}
onVectorEmbeddingChange={(
vectorEmbeddingPolicy: VectorEmbedding[],
vectorIndexingPolicy: VectorIndex[],
vectorPolicyValidated: boolean,
) => {
setVectorEmbeddingPolicy(vectorEmbeddingPolicy);
setVectorIndexingPolicy(vectorIndexingPolicy);
setVectorPolicyValidated(vectorPolicyValidated);
}}
/>
</Stack>
</Stack>
</CollapsibleSectionComponent>
</Stack>
);
};

View File

@@ -1,190 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
<form
className="panelFormWrapper"
id="panelGlobalSecondaryIndex"
onSubmit={[Function]}
>
<div
className="panelMainContent"
>
<Stack>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
* 
</span>
<Text
className="panelTextBold"
variant="small"
>
Source container id
</Text>
</Stack>
<Dropdown
onChange={[Function]}
placeholder="Choose source container"
style={
{
"fontSize": 12,
"width": 300,
}
}
styles={
{
"dropdownItem": {
"fontSize": 12,
},
"dropdownItemDisabled": {
"fontSize": 12,
},
"dropdownItemSelected": {
"fontSize": 12,
},
"title": {
"height": 27,
"lineHeight": 27,
},
}
}
/>
<Separator
className="panelSeparator"
/>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
* 
</span>
<Text
className="panelTextBold"
variant="small"
>
Global secondary index container id
</Text>
</Stack>
<input
aria-required={true}
autoComplete="off"
className="panelTextField"
id="globalSecondaryIndexId"
onChange={[Function]}
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
placeholder="e.g., indexbyEmailId"
required={true}
size={40}
title="May not end with space nor contain characters '\\' '/' '#' '?'"
type="text"
/>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
* 
</span>
<Text
className="panelTextBold"
variant="small"
>
Global secondary index definition
</Text>
<StyledTooltipHostBase
content={
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
target="blank"
>
Learn more about defining global secondary indexes.
</StyledLinkBase>
}
directionalHint={4}
>
<Icon
className="panelInfoIcon"
iconName="Info"
role="button"
tabIndex={0}
/>
</StyledTooltipHostBase>
</Stack>
<input
aria-required={true}
autoComplete="off"
className="panelTextField"
id="globalSecondaryIndexDefinition"
onChange={[Function]}
placeholder="SELECT c.email, c.accountId FROM c"
required={true}
size={40}
type="text"
value=""
/>
<PartitionKeyComponent
partitionKey=""
setPartitionKey={[Function]}
setSubPartitionKeys={[Function]}
subPartitionKeys={[]}
/>
<ThroughputComponent
globalSecondaryIndexThroughputOnChange={[Function]}
isCostAknowledgedOnChange={[Function]}
isGlobalSecondaryIndexAutoscaleOnChange={[Function]}
isSelectedSourceContainerSharedThroughput={[Function]}
setEnabledDedicatedThroughput={[Function]}
setIsThroughputCapExceeded={[Function]}
showCollectionThroughputInput={[Function]}
/>
<UniqueKeysComponent
setUniqueKeys={[Function]}
uniqueKeys={[]}
/>
<AnalyticalStoreComponent
explorer={
Explorer {
"_isInitializingNotebooks": false,
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
}
setEnableAnalyticalStore={[Function]}
/>
<AdvancedComponent
setSubPartitionKeys={[Function]}
setUseHashV1={[Function]}
/>
</Stack>
</div>
<PanelFooterComponent
buttonLabel="OK"
/>
</form>
`;

View File

@@ -7,7 +7,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
@@ -203,8 +202,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true} required={true}
autoComplete="off" autoComplete="off"
styles={getTextFieldStyles()} styles={getTextFieldStyles()}
pattern={ValidCosmosDbIdInputPattern.source} pattern="[^/?#\\-]*[^/?#- \\]"
title={ValidCosmosDbIdDescription} title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
placeholder="Type a new keyspace id" placeholder="Type a new keyspace id"
size={40} size={40}
value={newKeyspaceId} value={newKeyspaceId}
@@ -293,8 +292,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
required={true} required={true}
ariaLabel="addCollection-table Id Create table" ariaLabel="addCollection-table Id Create table"
autoComplete="off" autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source} pattern="[^/?#\\-]*[^/?#- \\]"
title={ValidCosmosDbIdDescription} title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
placeholder="Enter table Id" placeholder="Enter table Id"
size={20} size={20}
value={tableId} value={tableId}

View File

@@ -28,7 +28,6 @@ import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils"; import { getCollectionName } from "Utils/APITypeUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import * as React from "react"; import * as React from "react";
@@ -236,8 +235,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
aria-required aria-required
required required
autoComplete="off" autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source} pattern="[^/?#\\]*[^/?# \\]"
title={ValidCosmosDbIdDescription} title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder={`e.g., ${getCollectionName()}1`} placeholder={`e.g., ${getCollectionName()}1`}
size={40} size={40}
className="panelTextField" className="panelTextField"

View File

@@ -94,7 +94,6 @@
padding-left: @MediumSpace; padding-left: @MediumSpace;
.paneErrorLink { .paneErrorLink {
color: @LinkColor;
cursor: pointer; cursor: pointer;
font-size: @mediumFontSize; font-size: @mediumFontSize;
} }

View File

@@ -6,9 +6,7 @@ import {
Checkbox, Checkbox,
ChoiceGroup, ChoiceGroup,
DefaultButton, DefaultButton,
Dropdown,
IChoiceGroupOption, IChoiceGroupOption,
IDropdownOption,
ISpinButtonStyles, ISpinButtonStyles,
IToggleStyles, IToggleStyles,
Position, Position,
@@ -23,15 +21,7 @@ import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabric } from "Platform/Fabric/FabricUtil"; import { deleteAllStates } from "Shared/AppStatePersistenceUtility";
import {
AppStateComponentNames,
deleteAllStates,
deleteState,
hasState,
loadState,
saveState,
} from "Shared/AppStatePersistenceUtility";
import { import {
DefaultRUThreshold, DefaultRUThreshold,
LocalStorageUtility, LocalStorageUtility,
@@ -42,12 +32,10 @@ import {
} from "Shared/StorageUtility"; } from "Shared/StorageUtility";
import * as StringUtility from "Shared/StringUtility"; import * as StringUtility from "Shared/StringUtility";
import { updateUserContext, userContext } from "UserContext"; import { updateUserContext, userContext } from "UserContext";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
@@ -154,17 +142,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
? LocalStorageUtility.getEntryString(StorageKey.IsGraphAutoVizDisabled) ? LocalStorageUtility.getEntryString(StorageKey.IsGraphAutoVizDisabled)
: "false", : "false",
); );
const [selectedRegionalEndpoint, setSelectedRegionalEndpoint] = useState<string>(
hasState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
})
? (loadState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
}) as string)
: undefined,
);
const [retryAttempts, setRetryAttempts] = useState<number>( const [retryAttempts, setRetryAttempts] = useState<number>(
LocalStorageUtility.hasItem(StorageKey.RetryAttempts) LocalStorageUtility.hasItem(StorageKey.RetryAttempts)
? LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts) ? LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts)
@@ -197,64 +174,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const styles = useStyles(); const styles = useStyles();
const explorerVersion = configContext.gitSha; const explorerVersion = configContext.gitSha;
const isEmulator = configContext.platform === Platform.Emulator;
const shouldShowQueryPageOptions = userContext.apiType === "SQL"; const shouldShowQueryPageOptions = userContext.apiType === "SQL";
const showRetrySettings = const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") && const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
!isEmulator; const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator; const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
const showEnableEntraIdRbac =
isDataplaneRbacSupported(userContext.apiType) &&
userContext.authType === AuthType.AAD &&
configContext.platform !== Platform.Fabric &&
!isEmulator;
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
const uniqueAccountRegions = new Set<string>();
const regionOptions: IDropdownOption[] = [];
regionOptions.push({
key: userContext?.databaseAccount?.properties?.documentEndpoint,
text: `Global (Default)`,
data: {
isGlobal: true,
writeEnabled: true,
},
});
userContext?.databaseAccount?.properties?.writeLocations?.forEach((loc) => {
if (!uniqueAccountRegions.has(loc.locationName)) {
uniqueAccountRegions.add(loc.locationName);
regionOptions.push({
key: loc.documentEndpoint,
text: `${loc.locationName} (Read/Write)`,
data: {
isGlobal: false,
writeEnabled: true,
},
});
}
});
userContext?.databaseAccount?.properties?.readLocations?.forEach((loc) => {
if (!uniqueAccountRegions.has(loc.locationName)) {
uniqueAccountRegions.add(loc.locationName);
regionOptions.push({
key: loc.documentEndpoint,
text: `${loc.locationName} (Read)`,
data: {
isGlobal: false,
writeEnabled: false,
},
});
}
});
const shouldShowCopilotSampleDBOption = const shouldShowCopilotSampleDBOption =
userContext.apiType === "SQL" && userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotEnabled &&
useDatabases.getState().sampleDataResourceTokenCollection && useDatabases.getState().sampleDataResourceTokenCollection;
!isEmulator;
const handlerOnSubmit = async () => { const handlerOnSubmit = async () => {
setIsExecuting(true); setIsExecuting(true);
@@ -334,46 +262,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
} }
} }
const storedRegionalEndpoint = loadState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
}) as string;
const selectedRegionIsGlobal =
selectedRegionalEndpoint === userContext?.databaseAccount?.properties?.documentEndpoint;
if (selectedRegionIsGlobal && storedRegionalEndpoint) {
deleteState({
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
});
updateUserContext({
selectedRegionalEndpoint: undefined,
writeEnabledInSelectedRegion: true,
refreshCosmosClient: true,
});
useClientWriteEnabled.setState({ clientWriteEnabled: true });
} else if (
selectedRegionalEndpoint &&
!selectedRegionIsGlobal &&
selectedRegionalEndpoint !== storedRegionalEndpoint
) {
saveState(
{
componentName: AppStateComponentNames.SelectedRegionalEndpoint,
globalAccountName: userContext.databaseAccount?.name,
},
selectedRegionalEndpoint,
);
const validWriteEndpoint = userContext.databaseAccount?.properties?.writeLocations?.find(
(loc) => loc.documentEndpoint === selectedRegionalEndpoint,
);
updateUserContext({
selectedRegionalEndpoint: selectedRegionalEndpoint,
writeEnabledInSelectedRegion: !!validWriteEndpoint,
refreshCosmosClient: true,
});
useClientWriteEnabled.setState({ clientWriteEnabled: !!validWriteEndpoint });
}
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts); LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
@@ -523,10 +411,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setDefaultQueryResultsView(option.key as SplitterDirection); setDefaultQueryResultsView(option.key as SplitterDirection);
}; };
const handleOnSelectedRegionOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IDropdownOption): void => {
setSelectedRegionalEndpoint(option.key as string);
};
const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => { const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
const retryAttempts = Number(newValue); const retryAttempts = Number(newValue);
if (!isNaN(retryAttempts)) { if (!isNaN(retryAttempts)) {
@@ -607,7 +491,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
return ( return (
<RightPaneForm {...genericPaneProps}> <RightPaneForm {...genericPaneProps}>
<div className={`paneMainContent ${styles.container}`}> <div className={`paneMainContent ${styles.container}`}>
<Accordion className={`customAccordion ${styles.firstItem}`}> <Accordion className={styles.firstItem}>
{shouldShowQueryPageOptions && ( {shouldShowQueryPageOptions && (
<AccordionItem value="1"> <AccordionItem value="1">
<AccordionHeader> <AccordionHeader>
@@ -657,69 +541,41 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
)} )}
{showEnableEntraIdRbac && ( {userContext.apiType === "SQL" &&
<AccordionItem value="2"> userContext.authType === AuthType.AAD &&
<AccordionHeader> configContext.platform !== Platform.Fabric && (
<div className={styles.header}>Enable Entra ID RBAC</div> <AccordionItem value="2">
</AccordionHeader> <AccordionHeader>
<AccordionPanel> <div className={styles.header}>Enable Entra ID RBAC</div>
<div className={styles.settingsSectionContainer}> </AccordionHeader>
<div className={styles.settingsSectionDescription}> <AccordionPanel>
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID <div className={styles.settingsSectionContainer}>
RBAC. <div className={styles.settingsSectionDescription}>
<a Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer" ID RBAC.
target="_blank" <a
rel="noopener noreferrer" href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
> target="_blank"
{" "} rel="noopener noreferrer"
Learn more{" "} >
</a> {" "}
Learn more{" "}
</a>
</div>
<ChoiceGroup
ariaLabelledBy="enableDataPlaneRBACOptions"
options={dataPlaneRBACOptionsList}
styles={choiceButtonStyles}
selectedKey={enableDataPlaneRBACOption}
onChange={handleOnDataPlaneRBACOptionChange}
/>
</div> </div>
<ChoiceGroup </AccordionPanel>
ariaLabelledBy="enableDataPlaneRBACOptions" </AccordionItem>
options={dataPlaneRBACOptionsList} )}
styles={choiceButtonStyles} {userContext.apiType === "SQL" && (
selectedKey={enableDataPlaneRBACOption}
onChange={handleOnDataPlaneRBACOptionChange}
/>
</div>
</AccordionPanel>
</AccordionItem>
)}
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && (
<AccordionItem value="3">
<AccordionHeader>
<div className={styles.header}>Region Selection</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Changes region the Cosmos Client uses to access account.
</div>
<div>
<span className={styles.subHeader}>Select Region</span>
<InfoTooltip className={styles.headerIcon}>
Changes the account endpoint used to perform client operations.
</InfoTooltip>
</div>
<Dropdown
placeholder={
selectedRegionalEndpoint
? regionOptions.find((option) => option.key === selectedRegionalEndpoint)?.text
: regionOptions[0]?.text
}
onChange={handleOnSelectedRegionOptionChange}
options={regionOptions}
styles={{ root: { marginBottom: "10px" } }}
/>
</div>
</AccordionPanel>
</AccordionItem>
)}
{userContext.apiType === "SQL" && !isEmulator && (
<> <>
<AccordionItem value="4"> <AccordionItem value="3">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Query Timeout</div> <div className={styles.header}>Query Timeout</div>
</AccordionHeader> </AccordionHeader>
@@ -760,7 +616,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem value="5"> <AccordionItem value="4">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>RU Limit</div> <div className={styles.header}>RU Limit</div>
</AccordionHeader> </AccordionHeader>
@@ -794,7 +650,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem value="6"> <AccordionItem value="5">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Default Query Results View</div> <div className={styles.header}>Default Query Results View</div>
</AccordionHeader> </AccordionHeader>
@@ -815,9 +671,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem> </AccordionItem>
</> </>
)} )}
{(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") && (
{showRetrySettings && ( <AccordionItem value="6">
<AccordionItem value="7">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Retry Settings</div> <div className={styles.header}>Retry Settings</div>
</AccordionHeader> </AccordionHeader>
@@ -889,32 +744,31 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
)} )}
{!isEmulator && (
<AccordionItem value="8"> <AccordionItem value="7">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Enable container pagination</div> <div className={styles.header}>Enable container pagination</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order. Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable container pagination"
checked={containerPaginationEnabled}
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
label="Enable container pagination"
/>
</div> </div>
</AccordionPanel> <Checkbox
</AccordionItem> styles={{
)} label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable container pagination"
checked={containerPaginationEnabled}
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
label="Enable container pagination"
/>
</div>
</AccordionPanel>
</AccordionItem>
{shouldShowCrossPartitionOption && ( {shouldShowCrossPartitionOption && (
<AccordionItem value="9"> <AccordionItem value="8">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Enable cross-partition query</div> <div className={styles.header}>Enable cross-partition query</div>
</AccordionHeader> </AccordionHeader>
@@ -939,7 +793,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem> </AccordionItem>
)} )}
{shouldShowParallelismOption && ( {shouldShowParallelismOption && (
<AccordionItem value="10"> <AccordionItem value="9">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Max degree of parallelism</div> <div className={styles.header}>Max degree of parallelism</div>
</AccordionHeader> </AccordionHeader>
@@ -972,7 +826,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem> </AccordionItem>
)} )}
{shouldShowPriorityLevelOption && ( {shouldShowPriorityLevelOption && (
<AccordionItem value="11"> <AccordionItem value="10">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Priority Level</div> <div className={styles.header}>Priority Level</div>
</AccordionHeader> </AccordionHeader>
@@ -995,7 +849,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem> </AccordionItem>
)} )}
{shouldShowGraphAutoVizOption && ( {shouldShowGraphAutoVizOption && (
<AccordionItem value="12"> <AccordionItem value="11">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Display Gremlin query results as:&nbsp;</div> <div className={styles.header}>Display Gremlin query results as:&nbsp;</div>
</AccordionHeader> </AccordionHeader>
@@ -1016,7 +870,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem> </AccordionItem>
)} )}
{shouldShowCopilotSampleDBOption && ( {shouldShowCopilotSampleDBOption && (
<AccordionItem value="13"> <AccordionItem value="12">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Enable sample database</div> <div className={styles.header}>Enable sample database</div>
</AccordionHeader> </AccordionHeader>
@@ -1051,15 +905,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
"Clear History", "Clear History",
undefined, undefined,
"Are you sure you want to proceed?", "Are you sure you want to proceed?",
() => { () => deleteAllStates(),
deleteAllStates();
updateUserContext({
selectedRegionalEndpoint: undefined,
writeEnabledInSelectedRegion: true,
refreshCosmosClient: true,
});
useClientWriteEnabled.setState({ clientWriteEnabled: true });
},
"Cancel", "Cancel",
undefined, undefined,
<> <>
@@ -1070,7 +916,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<li>Reset your customized tab layout, including the splitter positions</li> <li>Reset your customized tab layout, including the splitter positions</li>
<li>Erase your table column preferences, including any custom columns</li> <li>Erase your table column preferences, including any custom columns</li>
<li>Clear your filter history</li> <li>Clear your filter history</li>
<li>Reset region selection to global</li>
</ul> </ul>
</>, </>,
); );

View File

@@ -11,7 +11,7 @@ exports[`Settings Pane should render Default properly 1`] = `
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl" className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
> >
<Accordion <Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx" className="___1uf6361_0000000 fz7g6wx"
> >
<AccordionItem <AccordionItem
value="1" value="1"
@@ -107,7 +107,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem <AccordionItem
value="4" value="3"
> >
<AccordionHeader> <AccordionHeader>
<div <div
@@ -148,7 +148,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem <AccordionItem
value="5" value="4"
> >
<AccordionHeader> <AccordionHeader>
<div <div
@@ -219,7 +219,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem <AccordionItem
value="6" value="5"
> >
<AccordionHeader> <AccordionHeader>
<div <div
@@ -281,7 +281,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem <AccordionItem
value="7" value="6"
> >
<AccordionHeader> <AccordionHeader>
<div <div
@@ -423,7 +423,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem <AccordionItem
value="8" value="7"
> >
<AccordionHeader> <AccordionHeader>
<div <div
@@ -459,7 +459,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem <AccordionItem
value="9" value="8"
> >
<AccordionHeader> <AccordionHeader>
<div <div
@@ -495,7 +495,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem <AccordionItem
value="10" value="9"
> >
<AccordionHeader> <AccordionHeader>
<div <div
@@ -572,10 +572,10 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl" className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
> >
<Accordion <Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx" className="___1uf6361_0000000 fz7g6wx"
> >
<AccordionItem <AccordionItem
value="7" value="6"
> >
<AccordionHeader> <AccordionHeader>
<div <div
@@ -717,7 +717,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem <AccordionItem
value="8" value="7"
> >
<AccordionHeader> <AccordionHeader>
<div <div
@@ -753,7 +753,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem <AccordionItem
value="12" value="11"
> >
<AccordionHeader> <AccordionHeader>
<div <div

View File

@@ -93,7 +93,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
id="newDatabaseId" id="newDatabaseId"
name="newDatabaseId" name="newDatabaseId"
onChange={[Function]} onChange={[Function]}
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]" pattern="[^/?#\\\\]*[^/?# \\\\]"
placeholder="Type a new database id" placeholder="Type a new database id"
required={true} required={true}
size={40} size={40}
@@ -178,7 +178,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
id="collectionId" id="collectionId"
name="collectionId" name="collectionId"
onChange={[Function]} onChange={[Function]}
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]" pattern="[^/?#\\\\]*[^/?# \\\\]"
placeholder="e.g., Container1" placeholder="e.g., Container1"
required={true} required={true}
size={40} size={40}
@@ -319,7 +319,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads. Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
<StyledLinkBase <StyledLinkBase
aria-label="Learn more about analytical store."
href="https://aka.ms/analytical-store-overview" href="https://aka.ms/analytical-store-overview"
target="_blank" target="_blank"
> >
@@ -384,7 +383,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
. Enable Synapse Link for this Cosmos DB account. . Enable Synapse Link for this Cosmos DB account.
<StyledLinkBase <StyledLinkBase
aria-label="Learn more about Azure Synapse Link."
className="capacitycalculator-link" className="capacitycalculator-link"
href="https://aka.ms/cosmosdb-synapselink" href="https://aka.ms/cosmosdb-synapselink"
target="_blank" target="_blank"

View File

@@ -18,7 +18,7 @@ import { createCollection } from "Common/dataAccess/createCollection";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator"; import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
import { PromptCard } from "Explorer/QueryCopilot/PromptCard"; import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { useCarousel } from "hooks/useCarousel"; import { useCarousel } from "hooks/useCarousel";

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { FeedbackLabels, HttpStatusCodes, NormalizedEventKey } from "Common/Constants"; import { HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import QueryError, { QueryErrorSeverity } from "Common/QueryError"; import QueryError, { QueryErrorSeverity } from "Common/QueryError";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
@@ -75,7 +75,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
const inputEdited = useRef(false); const inputEdited = useRef(false);
const itemRefs = useRef([]); const itemRefs = useRef([]);
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
const copyQueryRef = useRef(null);
const { const {
openFeedbackModal, openFeedbackModal,
hideFeedbackModalForLikedQueries, hideFeedbackModalForLikedQueries,
@@ -133,7 +132,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
document.body.removeChild(queryElement); document.body.removeChild(queryElement);
setshowCopyPopup(true); setshowCopyPopup(true);
copyQueryRef.current.focus();
setTimeout(() => { setTimeout(() => {
setshowCopyPopup(false); setshowCopyPopup(false);
}, 6000); }, 6000);
@@ -307,7 +305,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
if (isGeneratingQuery === null) { if (isGeneratingQuery === null) {
return " "; return " ";
} else if (isGeneratingQuery) { } else if (isGeneratingQuery) {
return "Thinking"; return "Content is loading";
} else { } else {
return "Content is updated"; return "Content is updated";
} }
@@ -393,7 +391,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}, },
}} }}
disabled={isGeneratingQuery} disabled={isGeneratingQuery}
autoComplete="off" autoComplete="list"
aria-expanded={showSamplePrompts}
placeholder="Ask a question in natural language and well generate the query for you." placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label" aria-labelledby="copilot-textfield-label"
onRenderSuffix={() => { onRenderSuffix={() => {
@@ -401,7 +400,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<IconButton <IconButton
iconProps={{ iconName: "Send" }} iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery || !userPrompt.trim()} disabled={isGeneratingQuery || !userPrompt.trim()}
allowDisabledFocus={true}
style={{ background: "none" }} style={{ background: "none" }}
onClick={() => startGenerateQueryProcess()} onClick={() => startGenerateQueryProcess()}
aria-label="Send" aria-label="Send"
@@ -579,7 +577,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}> <Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
{userContext.feedbackPolicies?.policyAllowFeedback && ( {userContext.feedbackPolicies?.policyAllowFeedback && (
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<Text style={{ fontSize: 12 }}>{FeedbackLabels.provideFeedback}</Text> <Text style={{ fontSize: 12 }}>Provide feedback</Text>
{showCallout && !hideFeedbackModalForLikedQueries && ( {showCallout && !hideFeedbackModalForLikedQueries && (
<Callout <Callout
role="status" role="status"
@@ -629,9 +627,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<IconButton <IconButton
id="likeBtn" id="likeBtn"
style={{ marginLeft: 10 }} style={{ marginLeft: 10 }}
aria-label={FeedbackLabels.provideFeedback} aria-label="Like"
role="button" role="toggle"
title="Like"
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }} iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => { onClick={() => {
setShowCallout(!likeQuery); setShowCallout(!likeQuery);
@@ -649,9 +646,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
/> />
<IconButton <IconButton
style={{ margin: "0 4px" }} style={{ margin: "0 4px" }}
role="button" role="toggle"
aria-label={FeedbackLabels.provideFeedback} aria-label="Dislike"
title="Dislike"
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }} iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => { onClick={() => {
let toggleStatusValue = "Unpressed"; let toggleStatusValue = "Unpressed";
@@ -680,7 +676,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
)} )}
<CommandBarButton <CommandBarButton
className="copyQuery" className="copyQuery"
elementRef={copyQueryRef}
onClick={copyGeneratedCode} onClick={copyGeneratedCode}
iconProps={{ iconName: "Copy" }} iconProps={{ iconName: "Copy" }}
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }} style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
@@ -711,9 +706,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
)} )}
</Stack> </Stack>
)} )}
{(showFeedbackBar || isGeneratingQuery) && (
<span role="alert" className="screenReaderOnly" aria-label={getAriaLabel()} />
)}
{isGeneratingQuery && ( {isGeneratingQuery && (
<ProgressIndicator <ProgressIndicator
label="Thinking..." label="Thinking..."

View File

@@ -1,6 +1,7 @@
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { import {
Areas, Areas,
BackendApi,
ConnectionStatusType, ConnectionStatusType,
ContainerStatusType, ContainerStatusType,
HttpStatusCodes, HttpStatusCodes,
@@ -31,6 +32,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getAuthorizationHeader } from "Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
@@ -80,7 +82,9 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
}; };
export const getCopilotEnabled = async (): Promise<boolean> => { 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 url = `${backendEndpoint}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();

View File

@@ -1,6 +1,5 @@
import { import {
Button, Button,
makeStyles,
Menu, Menu,
MenuButton, MenuButton,
MenuButtonProps, MenuButtonProps,
@@ -8,32 +7,26 @@ import {
MenuList, MenuList,
MenuPopover, MenuPopover,
MenuTrigger, MenuTrigger,
SplitButton,
makeStyles,
mergeClasses, mergeClasses,
shorthands, shorthands,
SplitButton,
} from "@fluentui/react-components"; } from "@fluentui/react-components";
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons"; import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
import { GlobalSecondaryIndexLabels } from "Common/Constants"; import { Platform, configContext } from "ConfigContext";
import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility";
import { configContext, Platform } from "ConfigContext";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel"; import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
import {
AddGlobalSecondaryIndexPanel,
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { Tabs } from "Explorer/Tabs/Tabs"; import { Tabs } from "Explorer/Tabs/Tabs";
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil"; import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
import { ResourceTree } from "Explorer/Tree/ResourceTree"; import { ResourceTree } from "Explorer/Tree/ResourceTree";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
import { Allotment, AllotmentHandle } from "allotment"; import { Allotment, AllotmentHandle } from "allotment";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { debounce } from "lodash"; import { debounce } from "lodash";
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
const useSidebarStyles = makeStyles({ const useSidebarStyles = makeStyles({
sidebar: { sidebar: {
@@ -116,7 +109,6 @@ interface GlobalCommand {
icon: JSX.Element; icon: JSX.Element;
onClick: () => void; onClick: () => void;
keyboardAction?: KeyboardAction; keyboardAction?: KeyboardAction;
ref?: React.RefObject<HTMLButtonElement>;
} }
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => { const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
@@ -126,11 +118,10 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
// However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu. // However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu.
// We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render. // We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render.
const [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null); const [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null);
const primaryFocusableRef = useRef<HTMLButtonElement>(null);
const actions = useMemo<GlobalCommand[]>(() => { const actions = useMemo<GlobalCommand[]>(() => {
if ( if (
(isFabric() && userContext.fabricContext?.isReadOnly) || configContext.platform === Platform.Fabric ||
userContext.apiType === "Postgres" || userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo" userContext.apiType === "VCoreMongo"
) { ) {
@@ -144,15 +135,12 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
id: "new_collection", id: "new_collection",
label: `New ${getCollectionName()}`, label: `New ${getCollectionName()}`,
icon: <Add16Regular />, icon: <Add16Regular />,
onClick: () => { onClick: () => explorer.onNewCollectionClicked(),
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
explorer.onNewCollectionClicked({ databaseId });
},
keyboardAction: KeyboardAction.NEW_COLLECTION, keyboardAction: KeyboardAction.NEW_COLLECTION,
}, },
]; ];
if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") { if (userContext.apiType !== "Tables") {
actions.push({ actions.push({
id: "new_database", id: "new_database",
label: `New ${getDatabaseName()}`, label: `New ${getDatabaseName()}`,
@@ -168,25 +156,6 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
}); });
} }
if (isGlobalSecondaryIndexEnabled()) {
const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = {
explorer,
};
actions.push({
id: "new_materialized_view",
label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
icon: <Add16Regular />,
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
<AddGlobalSecondaryIndexPanel {...addMaterializedViewPanelProps} />,
),
});
}
return actions; return actions;
}, [explorer]); }, [explorer]);
@@ -208,16 +177,6 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
); );
}, [actions, setKeyboardActions]); }, [actions, setKeyboardActions]);
useLayoutEffect(() => {
if (primaryFocusableRef.current) {
const timer = setTimeout(() => {
primaryFocusableRef.current.focus();
}, 0);
return () => clearTimeout(timer);
}
return undefined;
}, []);
if (!primaryAction) { if (!primaryAction) {
return null; return null;
} }
@@ -225,7 +184,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
return ( return (
<div className={styles.globalCommandsContainer} data-test="GlobalCommands"> <div className={styles.globalCommandsContainer} data-test="GlobalCommands">
{actions.length === 1 ? ( {actions.length === 1 ? (
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick} ref={primaryFocusableRef}> <Button icon={primaryAction.icon} onClick={onPrimaryActionClick}>
{primaryAction.label} {primaryAction.label}
</Button> </Button>
) : ( ) : (
@@ -235,7 +194,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
<div ref={setGlobalCommandButton}> <div ref={setGlobalCommandButton}>
<SplitButton <SplitButton
menuButton={{ ...triggerProps, "aria-label": "More commands" }} menuButton={{ ...triggerProps, "aria-label": "More commands" }}
primaryActionButton={{ onClick: onPrimaryActionClick, ref: primaryFocusableRef }} primaryActionButton={{ onClick: onPrimaryActionClick }}
className={styles.globalCommandsSplitButton} className={styles.globalCommandsSplitButton}
icon={primaryAction.icon} icon={primaryAction.icon}
> >
@@ -317,7 +276,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
}, [setLoading]); }, [setLoading]);
const hasGlobalCommands = !( const hasGlobalCommands = !(
isFabricMirrored() || configContext.platform === Platform.Fabric ||
userContext.apiType === "Postgres" || userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo" userContext.apiType === "VCoreMongo"
); );
@@ -340,18 +299,16 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
<> <>
<div className={styles.floatingControlsContainer}> <div className={styles.floatingControlsContainer}>
<div className={styles.floatingControls}> <div className={styles.floatingControls}>
{!isFabricNative() && ( <button
<button type="button"
type="button" data-test="Sidebar/RefreshButton"
data-test="Sidebar/RefreshButton" className={styles.floatingControlButton}
className={styles.floatingControlButton} disabled={loading}
disabled={loading} title="Refresh"
title="Refresh" onClick={onRefreshClick}
onClick={onRefreshClick} >
> <ArrowSync12Regular />
<ArrowSync12Regular /> </button>
</button>
)}
<button <button
type="button" type="button"
className={styles.floatingControlButton} className={styles.floatingControlButton}

View File

@@ -1,185 +0,0 @@
/**
* Accordion top class
*/
import { Link, makeStyles, tokens } from "@fluentui/react-components";
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import * as React from "react";
import { userContext } from "UserContext";
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
import LinkIcon from "../../../images/Link_blue.svg";
import Explorer from "../Explorer";
export interface SplashScreenProps {
explorer: Explorer;
}
const useStyles = makeStyles({
homeContainer: {
width: "100%",
alignContent: "center",
},
title: {
textAlign: "center",
fontSize: "20px",
fontWeight: "bold",
},
buttonsContainer: {
width: "584px",
margin: "auto",
display: "grid",
padding: "16px",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "10px",
gridAutoRows: "minmax(184px, auto)",
},
one: {
gridColumn: "1 / 3",
gridRow: "1 / 3",
"& svg": {
width: "48px",
height: "48px",
margin: "auto",
},
},
two: {
gridColumn: "3",
gridRow: "1",
"& img": {
width: "32px",
height: "32px",
margin: "auto",
},
},
three: {
gridColumn: "3",
gridRow: "2",
"& svg": {
width: "32px",
height: "32px",
margin: "auto",
},
},
buttonContainer: {
height: "100%",
display: "flex",
flexDirection: "column",
border: "1px solid #e0e0e0",
cursor: "pointer",
"&:hover": {
backgroundColor: tokens.colorNeutralBackground1Hover,
"border-color": tokens.colorNeutralStroke1Hover,
},
},
buttonUpperPart: {
textAlign: "center",
flexGrow: 1,
display: "flex",
backgroundColor: "#e3f7ef",
},
buttonLowerPart: {
borderTop: "1px solid #e0e0e0",
height: "76px",
padding: "8px",
"> div:nth-child(1)": {
fontWeight: "bold",
},
display: "flex",
flexDirection: "column",
justifyContent: "center",
},
footer: {
textAlign: "center",
},
});
interface FabricHomeScreenButtonProps {
title: string;
description: string;
icon: JSX.Element;
onClick?: () => void;
}
const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className: string }> = ({
title,
description,
icon,
className,
onClick,
}) => {
const styles = useStyles();
return (
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
<div className={styles.buttonUpperPart}>{icon}</div>
<div aria-label={title} className={styles.buttonLowerPart}>
<div>{title}</div>
<div>{description}</div>
</div>
</div>
);
};
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
const styles = useStyles();
const [openSampleDataImportDialog, setOpenSampleDataImportDialog] = React.useState(false);
const getSplashScreenButtons = (): JSX.Element => {
const buttons: FabricHomeScreenButtonProps[] = [
{
title: "New container",
description: "Create a destination container to store your data",
icon: <DocumentAddRegular />,
onClick: () => {
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
props.explorer.onNewCollectionClicked({ databaseId });
},
},
{
title: "Sample data",
description: "Automatically load sample data in your database",
icon: <img src={CosmosDbBlackIcon} />,
onClick: () => setOpenSampleDataImportDialog(true),
},
{
title: "App development",
description: "Start here to use an SDK to build your apps",
icon: <LinkMultipleRegular />,
onClick: () => window.open("https://aka.ms/cosmosdbfabricsdk", "_blank"),
},
];
return (
<div className={styles.buttonsContainer}>
<FabricHomeScreenButton className={styles.one} {...buttons[0]} />
<FabricHomeScreenButton className={styles.two} {...buttons[1]} />
<FabricHomeScreenButton className={styles.three} {...buttons[2]} />
</div>
);
};
const title = "Build your database";
return (
<>
<CosmosFluentProvider className={styles.homeContainer}>
<SampleDataImportDialog
open={openSampleDataImportDialog}
setOpen={setOpenSampleDataImportDialog}
explorer={props.explorer}
databaseName={userContext.fabricContext?.databaseName}
/>
<div className={styles.title} role="heading" aria-label={title}>
{title}
</div>
{getSplashScreenButtons()}
<div className={styles.footer}>
Need help?{" "}
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
Learn more <img src={LinkIcon} alt="Learn more" />
</Link>
</div>
</CosmosFluentProvider>
</>
);
};

View File

@@ -1,158 +0,0 @@
import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
makeStyles,
Spinner,
tokens,
} from "@fluentui/react-components";
import Explorer from "Explorer/Explorer";
import { checkContainerExists, createContainer, importData } from "Explorer/SplashScreen/SampleUtil";
import React, { useEffect, useState } from "react";
import * as ViewModels from "../../Contracts/ViewModels";
const SAMPLE_DATA_CONTAINER_NAME = "SampleData";
const useStyles = makeStyles({
dialogContent: {
alignItems: "center",
marginBottom: tokens.spacingVerticalL,
},
});
/**
* This dialog:
* - creates a container
* - imports data into the container
* @param props
* @returns
*/
export const SampleDataImportDialog: React.FC<{
open: boolean;
setOpen: (open: boolean) => void;
explorer: Explorer;
databaseName: string;
}> = (props) => {
const [status, setStatus] = useState<"idle" | "creating" | "importing" | "completed" | "error">("idle");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const containerName = SAMPLE_DATA_CONTAINER_NAME;
const [collection, setCollection] = useState<ViewModels.Collection>(undefined);
const styles = useStyles();
useEffect(() => {
// Reset state when dialog opens
if (props.open) {
setStatus("idle");
setErrorMessage(undefined);
}
}, [props.open]);
const handleStartImport = async (): Promise<void> => {
setStatus("creating");
const databaseName = props.databaseName;
if (checkContainerExists(databaseName, containerName)) {
const msg = `The container "${containerName}" in database "${databaseName}" already exists. Please delete it and retry.`;
setStatus("error");
setErrorMessage(msg);
return;
}
let collection;
try {
collection = await createContainer(databaseName, containerName, props.explorer);
} catch (error) {
setStatus("error");
setErrorMessage(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`);
return;
}
try {
setStatus("importing");
await importData(collection);
setCollection(collection);
setStatus("completed");
} catch (error) {
setStatus("error");
setErrorMessage(`Failed to import data: ${error instanceof Error ? error.message : String(error)}`);
}
};
const handleActionOnClick = () => {
switch (status) {
case "idle":
handleStartImport();
break;
case "error":
props.setOpen(false);
break;
case "creating":
case "importing":
props.setOpen(false);
break;
case "completed":
props.setOpen(false);
collection.openTab();
break;
}
};
const renderContent = () => {
switch (status) {
case "idle":
return `Create a container "${containerName}" and import sample data into it. This may take a few minutes.`;
case "creating":
return <Spinner size="small" labelPosition="above" label={`Creating container "${containerName}"...`} />;
case "importing":
return <Spinner size="small" labelPosition="above" label={`Importing data into "${containerName}"...`} />;
case "completed":
return `Successfully created "${containerName}" with sample data.`;
case "error":
return (
<div style={{ color: "red" }}>
<div>Error: {errorMessage}</div>
</div>
);
}
};
const getButtonLabel = () => {
switch (status) {
case "idle":
return "Start";
case "creating":
case "importing":
return "Close";
case "completed":
return "Close";
case "error":
return "Close";
}
};
return (
<Dialog open={props.open} onOpenChange={(event, data) => props.setOpen(data.open)}>
<DialogSurface>
<DialogBody>
<DialogTitle>Sample Data</DialogTitle>
<DialogContent>
<div className={styles.dialogContent}>{renderContent()}</div>
</DialogContent>
<DialogActions>
<Button
appearance="primary"
onClick={handleActionOnClick}
disabled={status === "creating" || status === "importing"}
>
{getButtonLabel()}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);
};

View File

@@ -1,56 +0,0 @@
import { createCollection } from "Common/dataAccess/createCollection";
import Explorer from "Explorer/Explorer";
import { useDatabases } from "Explorer/useDatabases";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
/**
* Public for unit tests
* @param databaseName
* @param containerName
* @param containerDatabases
*/
const hasContainer = (
databaseName: string,
containerName: string,
containerDatabases: ViewModels.Database[],
): boolean => {
const filteredDatabases = containerDatabases.filter((database) => database.id() === databaseName);
return (
filteredDatabases.length > 0 &&
filteredDatabases[0].collections().filter((collection) => collection.id() === containerName).length > 0
);
};
export const checkContainerExists = (databaseName: string, containerName: string) =>
hasContainer(databaseName, containerName, useDatabases.getState().databases);
export const createContainer = async (
databaseName: string,
containerName: string,
explorer: Explorer,
): Promise<ViewModels.Collection> => {
const createRequest: DataModels.CreateCollectionParams = {
createNewDatabase: false,
collectionId: containerName,
databaseId: databaseName,
databaseLevelThroughput: false,
};
await createCollection(createRequest);
await explorer.refreshAllDatabases();
const database = useDatabases.getState().findDatabaseWithId(databaseName);
if (!database) {
return undefined;
}
await database.loadCollections();
const newCollection = database.findCollectionWithId(containerName);
return newCollection;
};
export const importData = async (collection: ViewModels.Collection): Promise<void> => {
// TODO: keep same chunk as ContainerSampleGenerator
const dataFileContent = await import(
/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json"
);
await collection.bulkInsertDocuments(dataFileContent.data);
};

View File

@@ -39,7 +39,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
role="button" role="button"
> >
<div> <div>
<img src={imgSrc} alt={title} aria-hidden="true" /> <img src={imgSrc} />
</div> </div>
<Stack style={{ marginLeft: 16 }}> <Stack style={{ marginLeft: 16 }}>
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text> <Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>

View File

@@ -3,7 +3,7 @@ import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { CassandraProxyAPIs } from "../../Common/Constants"; import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
import { handleError } from "../../Common/ErrorHandlingUtils"; import { handleError } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
@@ -264,6 +264,9 @@ export class CassandraAPIDataClient extends TableDataClient {
shouldNotify?: boolean, shouldNotify?: boolean,
paginationToken?: string, paginationToken?: string,
): Promise<Entities.IListTableEntitiesResult> { ): Promise<Entities.IListTableEntitiesResult> {
if (!this.useCassandraProxyEndpoint("postQuery")) {
return this.queryDocuments_ToBeDeprecated(collection, query, shouldNotify, paginationToken);
}
const clearMessage = const clearMessage =
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
try { 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( public async deleteDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[], entitiesToDelete: Entities.ITableEntity[],
@@ -419,6 +471,10 @@ export class CassandraAPIDataClient extends TableDataClient {
} }
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> { public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
if (!this.useCassandraProxyEndpoint("getKeys")) {
return this.getTableKeys_ToBeDeprecated(collection);
}
if (!!collection.cassandraKeys) { if (!!collection.cassandraKeys) {
return Q.resolve(collection.cassandraKeys); return Q.resolve(collection.cassandraKeys);
} }
@@ -459,7 +515,52 @@ export class CassandraAPIDataClient extends TableDataClient {
return deferred.promise; 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[]> { public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
if (!this.useCassandraProxyEndpoint("getSchema")) {
return this.getTableSchema_ToBeDeprecated(collection);
}
if (!!collection.cassandraSchema) { if (!!collection.cassandraSchema) {
return Q.resolve(collection.cassandraSchema); return Q.resolve(collection.cassandraSchema);
} }
@@ -501,7 +602,52 @@ export class CassandraAPIDataClient extends TableDataClient {
return deferred.promise; 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> { 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 deferred = Q.defer();
const { authType, databaseAccount } = userContext; const { authType, databaseAccount } = userContext;
const apiEndpoint: string = const apiEndpoint: string =
@@ -531,6 +677,38 @@ export class CassandraAPIDataClient extends TableDataClient {
return deferred.promise; 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 { private trimCassandraEndpoint(cassandraEndpoint: string): string {
if (!cassandraEndpoint) { if (!cassandraEndpoint) {
return cassandraEndpoint; return cassandraEndpoint;
@@ -569,4 +747,23 @@ export class CassandraAPIDataClient extends TableDataClient {
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string { private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
return collection.cassandraKeys.partitionKeys[0].property; 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)
);
}
} }

Some files were not shown because too many files have changed in this diff Show More