mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 19:01:28 +00:00
Compare commits
35 Commits
unit-tests
...
users/aisa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ba75c0594 | ||
|
|
c142e4ad8a | ||
|
|
fb95b44242 | ||
|
|
3cd6d5a65d | ||
|
|
d924824536 | ||
|
|
cd27814fad | ||
|
|
909957a9a1 | ||
|
|
569e5ed1fc | ||
|
|
a5c3e6bea0 | ||
|
|
76e63818d3 | ||
|
|
cfb5db4df6 | ||
|
|
922ca5c523 | ||
|
|
bafe002fa3 | ||
|
|
0817acf404 | ||
|
|
8e2c46301d | ||
|
|
012d043c78 | ||
|
|
3afd74a957 | ||
|
|
0ef4399ba4 | ||
|
|
870863a723 | ||
|
|
e3815734db | ||
|
|
5ea78f9abf | ||
|
|
8a56214ec2 | ||
|
|
e3ae006100 | ||
|
|
589b61afaf | ||
|
|
eb3f6bc93f | ||
|
|
6ec909a97b | ||
|
|
08a51ca6b1 | ||
|
|
30a3b5c7a4 | ||
|
|
f370507a27 | ||
|
|
e0edaf405c | ||
|
|
f8231600d6 | ||
|
|
45c8d70c77 | ||
|
|
70d7ee755b | ||
|
|
0a4aed4f47 | ||
|
|
a7d007e0dd |
@@ -23,8 +23,6 @@ src/Common/MongoUtility.ts
|
|||||||
src/Common/NotificationsClientBase.ts
|
src/Common/NotificationsClientBase.ts
|
||||||
src/Common/QueriesClient.ts
|
src/Common/QueriesClient.ts
|
||||||
src/Common/Splitter.ts
|
src/Common/Splitter.ts
|
||||||
src/Controls/Heatmap/Heatmap.test.ts
|
|
||||||
src/Controls/Heatmap/Heatmap.ts
|
|
||||||
src/Definitions/datatables.d.ts
|
src/Definitions/datatables.d.ts
|
||||||
src/Definitions/gif.d.ts
|
src/Definitions/gif.d.ts
|
||||||
src/Definitions/globals.d.ts
|
src/Definitions/globals.d.ts
|
||||||
|
|||||||
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@@ -177,9 +177,39 @@ jobs:
|
|||||||
- name: "Az CLI login"
|
- name: "Az CLI login"
|
||||||
uses: Azure/login@v2
|
uses: Azure/login@v2
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
|
||||||
|
- name: "Acquire RBAC tokens for test accounts"
|
||||||
|
uses: azure/cli@v2
|
||||||
|
with:
|
||||||
|
azcliversion: latest
|
||||||
|
inlineScript: |
|
||||||
|
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
|
||||||
|
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
||||||
|
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
||||||
|
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
|
||||||
|
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
|
||||||
|
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
|
||||||
|
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
|
||||||
|
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
||||||
|
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
- name: Upload blob report to GitHub Actions Artifacts
|
||||||
|
|||||||
2
.github/workflows/cleanup.yml
vendored
2
.github/workflows/cleanup.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: "Az CLI login"
|
- name: "Az CLI login"
|
||||||
uses: azure/login@v1
|
uses: azure/login@v1
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
|
|||||||
2
.npmrc
2
.npmrc
@@ -1,4 +1,4 @@
|
|||||||
save-exact=true
|
save-exact=true
|
||||||
|
|
||||||
# Ignore peer dependency conflicts
|
# Ignore peer dependency conflicts
|
||||||
force=true # TODO: Remove this when we update to React 17 or higher!
|
force=true # TODO: Remove this when we update to React 17 or higher!
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||||
"isTerminalEnabled": true,
|
|
||||||
"isPhoenixEnabled": true
|
"isPhoenixEnabled": true
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||||
"isTerminalEnabled" : false,
|
"isPhoenixEnabled": false
|
||||||
"isPhoenixEnabled" : false
|
}
|
||||||
}
|
|
||||||
1
images/AzureOpenAi.svg
Normal file
1
images/AzureOpenAi.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="uuid-adbdae8e-5a41-46d1-8c18-aa73cdbfee32" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><path d="m0,2.7v12.6c0,1.491,1.209,2.7,2.7,2.7h12.6c1.491,0,2.7-1.209,2.7-2.7V2.7c0-1.491-1.209-2.7-2.7-2.7H2.7C1.209,0,0,1.209,0,2.7ZM10.8,0v3.6c0,3.976,3.224,7.2,7.2,7.2h-3.6c-3.976,0-7.199,3.222-7.2,7.198v-3.598c0-3.976-3.224-7.2-7.2-7.2h3.6c3.976,0,7.2-3.224,7.2-7.2Z" fill="#000000" stroke-width="0" /></svg>
|
||||||
|
After Width: | Height: | Size: 443 B |
3
images/github-black-and-white.svg
Normal file
3
images/github-black-and-white.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 0C8.73438 0 9.44271 0.0960961 10.125 0.288288C10.8073 0.48048 11.4427 0.758091 12.0312 1.12112C12.6198 1.48415 13.1589 1.91124 13.6484 2.4024C14.138 2.89356 14.5573 3.44611 14.9062 4.06006C15.2552 4.67401 15.5234 5.32799 15.7109 6.02202C15.8984 6.71605 15.9948 7.44211 16 8.2002C16 9.08108 15.8698 9.92993 15.6094 10.7467C15.349 11.5636 14.9766 12.3136 14.4922 12.997C14.0078 13.6803 13.4323 14.2783 12.7656 14.7908C12.099 15.3033 11.3542 15.701 10.5312 15.984C10.5156 15.9893 10.4922 15.992 10.4609 15.992C10.4297 15.992 10.4062 15.9947 10.3906 16C10.2656 16 10.1667 15.9626 10.0938 15.8879C10.0208 15.8131 9.98438 15.7144 9.98438 15.5916V14.4705C9.98438 14.1021 9.98698 13.7257 9.99219 13.3413C9.99219 13.0691 9.95312 12.7941 9.875 12.5165C9.79688 12.2389 9.65625 12.0067 9.45312 11.8198C10.0573 11.7504 10.5859 11.625 11.0391 11.4434C11.4922 11.2619 11.8724 11.0057 12.1797 10.6747C12.487 10.3437 12.7161 9.94328 12.8672 9.47347C13.0182 9.00367 13.0964 8.43777 13.1016 7.77578C13.1016 7.35936 13.0339 6.96697 12.8984 6.5986C12.763 6.23023 12.5573 5.88856 12.2812 5.57357C12.3385 5.42409 12.3802 5.26927 12.4062 5.10911C12.4323 4.94895 12.4453 4.78879 12.4453 4.62863C12.4453 4.42042 12.4245 4.21488 12.3828 4.01201C12.3411 3.80914 12.2812 3.60627 12.2031 3.4034C12.1771 3.39273 12.1484 3.38739 12.1172 3.38739C12.0859 3.38739 12.0573 3.38739 12.0312 3.38739C11.8646 3.38739 11.6901 3.41408 11.5078 3.46747C11.3255 3.52085 11.1458 3.59026 10.9688 3.67568C10.7917 3.76109 10.6172 3.85452 10.4453 3.95596C10.2734 4.05739 10.125 4.15349 10 4.24424C9.34896 4.05739 8.68229 3.96396 8 3.96396C7.31771 3.96396 6.65104 4.05739 6 4.24424C5.86979 4.15349 5.72135 4.05739 5.55469 3.95596C5.38802 3.85452 5.21615 3.76376 5.03906 3.68368C4.86198 3.6036 4.67969 3.5342 4.49219 3.47548C4.30469 3.41675 4.13021 3.38739 3.96875 3.38739H3.88281C3.85156 3.38739 3.82292 3.39273 3.79688 3.4034C3.72396 3.60093 3.66667 3.80113 3.625 4.004C3.58333 4.20687 3.5599 4.41508 3.55469 4.62863C3.55469 4.78879 3.56771 4.94895 3.59375 5.10911C3.61979 5.26927 3.66146 5.42409 3.71875 5.57357C3.44271 5.88322 3.23698 6.22222 3.10156 6.59059C2.96615 6.95896 2.89844 7.35402 2.89844 7.77578C2.89844 8.42709 2.97396 8.99032 3.125 9.46547C3.27604 9.94061 3.50521 10.341 3.8125 10.6667C4.11979 10.9923 4.5 11.2513 4.95312 11.4434C5.40625 11.6356 5.9349 11.7638 6.53906 11.8278C6.38802 11.9666 6.27344 12.1321 6.19531 12.3243C6.11719 12.5165 6.0625 12.7167 6.03125 12.9249C5.89062 12.9943 5.74219 13.0477 5.58594 13.0851C5.42969 13.1225 5.27344 13.1411 5.11719 13.1411C4.78385 13.1411 4.50781 13.0611 4.28906 12.9009C4.07031 12.7407 3.875 12.5219 3.70312 12.2442C3.64062 12.1428 3.5651 12.0414 3.47656 11.9399C3.38802 11.8385 3.29167 11.7477 3.1875 11.6677C3.08333 11.5876 2.97135 11.5235 2.85156 11.4755C2.73177 11.4274 2.60677 11.4007 2.47656 11.3954H2.38281C2.34115 11.3954 2.30208 11.4034 2.26562 11.4194C2.22917 11.4354 2.19271 11.4515 2.15625 11.4675C2.11979 11.4835 2.10417 11.5102 2.10938 11.5475C2.10938 11.6116 2.14583 11.673 2.21875 11.7317C2.29167 11.7905 2.35156 11.8385 2.39844 11.8759L2.42188 11.8919C2.53646 11.9826 2.63542 12.0681 2.71875 12.1481C2.80208 12.2282 2.88021 12.3163 2.95312 12.4124C3.02604 12.5085 3.08594 12.6099 3.13281 12.7167C3.17969 12.8235 3.23958 12.9489 3.3125 13.0931C3.48958 13.5095 3.73698 13.8111 4.05469 13.998C4.3724 14.1849 4.75521 14.2809 5.20312 14.2863C5.33854 14.2863 5.47396 14.2783 5.60938 14.2623C5.74479 14.2462 5.88021 14.2222 6.01562 14.1902V15.5836C6.01562 15.7117 5.97917 15.8131 5.90625 15.8879C5.83333 15.9626 5.73177 16 5.60156 16H5.53906C5.51302 16 5.48958 15.9947 5.46875 15.984C4.65104 15.7117 3.90625 15.3193 3.23438 14.8068C2.5625 14.2943 1.98698 13.6937 1.50781 13.005C1.02865 12.3163 0.658854 11.5636 0.398438 10.7467C0.138021 9.92993 0.00520833 9.08108 0 8.2002C0 7.44745 0.09375 6.72139 0.28125 6.02202C0.46875 5.32266 0.739583 4.67134 1.09375 4.06807C1.44792 3.4648 1.86458 2.91225 2.34375 2.41041C2.82292 1.90858 3.36198 1.47881 3.96094 1.12112C4.5599 0.76343 5.19792 0.488488 5.875 0.296296C6.55208 0.104104 7.26042 0.00533867 8 0Z" fill="#000000"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
37
package-lock.json
generated
37
package-lock.json
generated
@@ -10,7 +10,7 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "4.3.0",
|
"@azure/cosmos": "4.5.0",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "4.5.0",
|
"@azure/identity": "4.5.0",
|
||||||
"@azure/msal-browser": "2.14.2",
|
"@azure/msal-browser": "2.14.2",
|
||||||
@@ -391,24 +391,25 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@azure/cosmos": {
|
"node_modules/@azure/cosmos": {
|
||||||
"version": "4.3.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.5.0.tgz",
|
||||||
"integrity": "sha512-0Ls3l1uWBBSphx6YRhnM+w7rSvq8qVugBCdO6kSiNuRYXEf6+YWLjbzz4e7L2kkz/6ScFdZIOJYP+XtkiRYOhA==",
|
"integrity": "sha512-JsTh4twb6FcwP7rJwxQiNZQ/LGtuF6gmciaxY9Rnp6/A325Lhsw/SH4R2ArpT0yCvozbZpweIwdPfUkXVBtp5w==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^2.0.0",
|
"@azure/abort-controller": "^2.1.2",
|
||||||
"@azure/core-auth": "^1.7.1",
|
"@azure/core-auth": "^1.9.0",
|
||||||
"@azure/core-rest-pipeline": "^1.15.1",
|
"@azure/core-rest-pipeline": "^1.19.1",
|
||||||
"@azure/core-tracing": "^1.1.1",
|
"@azure/core-tracing": "^1.2.0",
|
||||||
"@azure/core-util": "^1.8.1",
|
"@azure/core-util": "^1.11.0",
|
||||||
"@azure/keyvault-keys": "^4.8.0",
|
"@azure/keyvault-keys": "^4.9.0",
|
||||||
|
"@azure/logger": "^1.1.4",
|
||||||
"fast-json-stable-stringify": "^2.1.0",
|
"fast-json-stable-stringify": "^2.1.0",
|
||||||
"jsbi": "^4.3.0",
|
|
||||||
"priorityqueuejs": "^2.0.0",
|
"priorityqueuejs": "^2.0.0",
|
||||||
"semaphore": "^1.1.0",
|
"semaphore": "^1.1.0",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/cosmos-language-service": {
|
"node_modules/@azure/cosmos-language-service": {
|
||||||
@@ -438,8 +439,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/cosmos/node_modules/tslib": {
|
"node_modules/@azure/cosmos/node_modules/tslib": {
|
||||||
"version": "2.6.2",
|
"version": "2.8.1",
|
||||||
"license": "0BSD"
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
},
|
},
|
||||||
"node_modules/@azure/identity": {
|
"node_modules/@azure/identity": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
@@ -27178,11 +27180,6 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsbi": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g=="
|
|
||||||
},
|
|
||||||
"node_modules/jsbn": {
|
"node_modules/jsbn": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "4.3.0",
|
"@azure/cosmos": "4.5.0",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "4.5.0",
|
"@azure/identity": "4.5.0",
|
||||||
"@azure/msal-browser": "2.14.2",
|
"@azure/msal-browser": "2.14.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[defaults]
|
[defaults]
|
||||||
group = dataexplorer-preview
|
group = dataexplorer-preview
|
||||||
sku = P1V2
|
sku = P1v2
|
||||||
appserviceplan = dataexplorer-preview
|
appserviceplan = dataexplorer-preview
|
||||||
location = westus2
|
location = westus2
|
||||||
web = dataexplorer-preview
|
web = dataexplorer-preview
|
||||||
|
|||||||
36205
preview/package-lock.json
generated
36205
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,18 @@
|
|||||||
"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 \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:20-lts\" --sku P1V2",
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Microsoft Corporation",
|
"author": "Microsoft Corporation",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.17.1",
|
"body-parser": "^1.20.3",
|
||||||
|
"express": "^4.21.2",
|
||||||
"http-proxy-middleware": "^3.0.3",
|
"http-proxy-middleware": "^3.0.3",
|
||||||
"node": "^18.20.6",
|
"node": "^20.19.5",
|
||||||
"node-fetch": "^2.6.1"
|
"node-fetch": "^2.6.1",
|
||||||
|
"path-to-regexp": "^0.1.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65616
sampleData/fabricSampleData.json
Normal file
65616
sampleData/fabricSampleData.json
Normal file
File diff suppressed because it is too large
Load Diff
322616
sampleData/fabricSampleDataVectors.json
Normal file
322616
sampleData/fabricSampleDataVectors.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -138,13 +138,12 @@ export enum MongoBackendEndpointType {
|
|||||||
remote,
|
remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BackendApi {
|
export class AadScopeEndpoints {
|
||||||
public static readonly GenerateToken: string = "GenerateToken";
|
public static readonly Development: string = "https://cosmos.azure.com";
|
||||||
public static readonly PortalSettings: string = "PortalSettings";
|
public static readonly MPAC: string = "https://cosmos.azure.com";
|
||||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
public static readonly Prod: string = "https://cosmos.azure.com";
|
||||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
public static readonly Fairfax: string = "https://cosmos.azure.us";
|
||||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
public static readonly Mooncake: string = "https://cosmos.azure.cn";
|
||||||
public static readonly SampleData: string = "SampleData";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
export class PortalBackendEndpoints {
|
||||||
@@ -264,6 +263,7 @@ export class HttpHeaders {
|
|||||||
public static activityId: string = "x-ms-activity-id";
|
public static activityId: string = "x-ms-activity-id";
|
||||||
public static apiType: string = "x-ms-cosmos-apitype";
|
public static apiType: string = "x-ms-cosmos-apitype";
|
||||||
public static authorization: string = "authorization";
|
public static authorization: string = "authorization";
|
||||||
|
public static entraIdToken: string = "x-ms-entraid-token";
|
||||||
public static collectionIndexTransformationProgress: string =
|
public static collectionIndexTransformationProgress: string =
|
||||||
"x-ms-documentdb-collection-index-transformation-progress";
|
"x-ms-documentdb-collection-index-transformation-progress";
|
||||||
public static continuation: string = "x-ms-continuation";
|
public static continuation: string = "x-ms-continuation";
|
||||||
@@ -774,3 +774,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
|
|||||||
|
|
||||||
userPrompt: "find all products",
|
userPrompt: "find all products",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum MongoGuidRepresentation {
|
||||||
|
Standard = "Standard",
|
||||||
|
CSharpLegacy = "CSharpLegacy",
|
||||||
|
JavaLegacy = "JavaLegacy",
|
||||||
|
PythonLegacy = "PythonLegacy",
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
|||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
import { PriorityLevel } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
||||||
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
@@ -20,8 +20,7 @@ const _global = typeof self === "undefined" ? window : self;
|
|||||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
|
|
||||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
if (useDataplaneRbacAuthorization(userContext)) {
|
||||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||||
"Explorer/tokenProvider",
|
"Explorer/tokenProvider",
|
||||||
|
|||||||
@@ -28,3 +28,39 @@ describe("Environment Utility Test", () => {
|
|||||||
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Development);
|
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Development);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("normalizeArmEndpoint", () => {
|
||||||
|
it("should append '/' if not present", () => {
|
||||||
|
expect(EnvironmentUtility.normalizeArmEndpoint("https://example.com")).toBe("https://example.com/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the same uri if '/' is present at the end", () => {
|
||||||
|
expect(EnvironmentUtility.normalizeArmEndpoint("https://example.com/")).toBe("https://example.com/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty string", () => {
|
||||||
|
expect(EnvironmentUtility.normalizeArmEndpoint("")).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getEnvironment", () => {
|
||||||
|
it("should return Prod environment", () => {
|
||||||
|
updateConfigContext({
|
||||||
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
|
});
|
||||||
|
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Prod);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Fairfax environment", () => {
|
||||||
|
updateConfigContext({
|
||||||
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Fairfax,
|
||||||
|
});
|
||||||
|
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Fairfax);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return Mooncake environment", () => {
|
||||||
|
updateConfigContext({
|
||||||
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mooncake,
|
||||||
|
});
|
||||||
|
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Mooncake);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PortalBackendEndpoints } from "Common/Constants";
|
import { AadScopeEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||||
|
import * as Logger from "Common/Logger";
|
||||||
import { configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
|
|
||||||
export function normalizeArmEndpoint(uri: string): string {
|
export function normalizeArmEndpoint(uri: string): string {
|
||||||
@@ -27,3 +28,17 @@ export const getEnvironment = (): Environment => {
|
|||||||
|
|
||||||
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
|
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getEnvironmentScopeEndpoint = (): string => {
|
||||||
|
const environment = getEnvironment();
|
||||||
|
const endpoint = AadScopeEndpoints[environment];
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new Error("Cannot determine AAD scope endpoint");
|
||||||
|
}
|
||||||
|
const hrefEndpoint = new URL(endpoint).href.replace(/\/+$/, "/.default");
|
||||||
|
Logger.logInfo(
|
||||||
|
`Using AAD scope endpoint: ${hrefEndpoint}, Environment: ${environment}`,
|
||||||
|
"EnvironmentUtility/getEnvironmentScopeEndpoint",
|
||||||
|
);
|
||||||
|
return hrefEndpoint;
|
||||||
|
};
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -84,7 +83,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
queryDocuments(databaseId, collection, true, "{}");
|
queryDocuments(databaseId, collection, true, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -101,7 +99,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -120,7 +117,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -137,7 +133,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -156,7 +151,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -173,7 +167,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -197,7 +190,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -216,7 +208,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
deleteDocuments(databaseId, collection, [documentId]);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -233,7 +224,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
|
import { getMongoGuidRepresentation } from "Shared/StorageUtility";
|
||||||
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";
|
||||||
@@ -6,6 +7,7 @@ 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 { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
import { isDataplaneRbacEnabledForProxyApi } from "../Utils/AuthorizationUtils";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
@@ -21,7 +23,13 @@ function authHeaders() {
|
|||||||
if (userContext.authType === AuthType.EncryptedToken) {
|
if (userContext.authType === AuthType.EncryptedToken) {
|
||||||
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
|
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
|
||||||
} else {
|
} else {
|
||||||
return { [HttpHeaders.authorization]: userContext.authorizationToken };
|
const headers: { [key: string]: string } = {
|
||||||
|
[HttpHeaders.authorization]: userContext.authorizationToken,
|
||||||
|
};
|
||||||
|
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||||
|
headers[HttpHeaders.entraIdToken] = userContext.aadToken;
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +147,9 @@ export function readDocument(
|
|||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
? documentId.partitionKeyProperties?.[0]
|
? documentId.partitionKeyProperties?.[0]
|
||||||
: "",
|
: "",
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
@@ -181,6 +192,9 @@ export function createDocument(
|
|||||||
partitionKey:
|
partitionKey:
|
||||||
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||||
documentContent: JSON.stringify(documentContent),
|
documentContent: JSON.stringify(documentContent),
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
@@ -228,6 +242,9 @@ export function updateDocument(
|
|||||||
? documentId.partitionKeyProperties?.[0]
|
? documentId.partitionKeyProperties?.[0]
|
||||||
: "",
|
: "",
|
||||||
documentContent,
|
documentContent,
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
@@ -274,6 +291,9 @@ export function deleteDocuments(
|
|||||||
subscriptionID: userContext.subscriptionId,
|
subscriptionID: userContext.subscriptionId,
|
||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseAccountName: databaseAccount.name,
|
databaseAccountName: databaseAccount.name,
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
||||||
{
|
{
|
||||||
"disableNonStreamingOrderByQuery": true,
|
|
||||||
"enableQueryControl": false,
|
"enableQueryControl": false,
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
@@ -14,7 +13,6 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] =
|
|||||||
|
|
||||||
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
||||||
{
|
{
|
||||||
"disableNonStreamingOrderByQuery": true,
|
|
||||||
"enableQueryControl": false,
|
"enableQueryControl": false,
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
||||||
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
|
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
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";
|
||||||
@@ -43,6 +45,14 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
|||||||
}
|
}
|
||||||
|
|
||||||
logConsoleInfo(`Successfully created container ${params.collectionId}`);
|
logConsoleInfo(`Successfully created container ${params.collectionId}`);
|
||||||
|
|
||||||
|
if (isFabricNative()) {
|
||||||
|
sendMessage({
|
||||||
|
type: FabricMessageTypes.ContainerUpdated,
|
||||||
|
params: { updateType: "created" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return collection;
|
return collection;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "CreateCollection", `Error while creating container ${params.collectionId}`);
|
handleError(error, "CreateCollection", `Error while creating container ${params.collectionId}`);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
|
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -19,6 +21,11 @@ export async function deleteCollection(databaseId: string, collectionId: string)
|
|||||||
await client().database(databaseId).container(collectionId).delete();
|
await client().database(databaseId).container(collectionId).delete();
|
||||||
}
|
}
|
||||||
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
||||||
|
|
||||||
|
sendMessage({
|
||||||
|
type: FabricMessageTypes.ContainerUpdated,
|
||||||
|
params: { updateType: "deleted" },
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "DeleteCollection", `Error while deleting container ${collectionId}`);
|
handleError(error, "DeleteCollection", `Error while deleting container ${collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -41,7 +42,7 @@ interface MetricsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||||
if (userContext.authType !== AuthType.AAD) {
|
if (userContext.authType !== AuthType.AAD || isFabricNative()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
|
||||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
import { Queries } from "../Constants";
|
import { Queries } from "../Constants";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
@@ -28,6 +27,5 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
|||||||
Queries.itemsPerPage;
|
Queries.itemsPerPage;
|
||||||
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
|
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
|
||||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||||
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
|
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Item, RequestOptions } from "@azure/cosmos";
|
import { Item, RequestOptions } from "@azure/cosmos";
|
||||||
import { HttpHeaders } from "Common/Constants";
|
import { HttpHeaders } from "Common/Constants";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
@@ -23,10 +24,17 @@ export const updateDocument = async (
|
|||||||
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
|
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
// If user has chosen to ignore partition key on update, pass null instead of actual partition key value
|
||||||
|
const ignorePartitionKeyOnDocumentUpdateFlag = LocalStorageUtility.getEntryBoolean(
|
||||||
|
StorageKey.IgnorePartitionKeyOnDocumentUpdate,
|
||||||
|
);
|
||||||
|
const partitionKey = ignorePartitionKeyOnDocumentUpdateFlag ? undefined : getPartitionKeyValue(documentId);
|
||||||
|
|
||||||
const response = await client()
|
const response = await client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.item(documentId.id(), getPartitionKeyValue(documentId))
|
.item(documentId.id(), partitionKey)
|
||||||
.replace(newDocument, options);
|
.replace(newDocument, options);
|
||||||
|
|
||||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
|
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||||
import {
|
import {
|
||||||
BackendApi,
|
|
||||||
CassandraProxyEndpoints,
|
|
||||||
JunoEndpoints,
|
|
||||||
MongoProxyEndpoints,
|
|
||||||
PortalBackendEndpoints,
|
|
||||||
} from "Common/Constants";
|
|
||||||
import {
|
|
||||||
allowedAadEndpoints,
|
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
|
defaultAllowedAadEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
defaultAllowedCassandraProxyEndpoints,
|
defaultAllowedCassandraProxyEndpoints,
|
||||||
|
defaultAllowedGraphEndpoints,
|
||||||
defaultAllowedMongoProxyEndpoints,
|
defaultAllowedMongoProxyEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
@@ -29,6 +23,8 @@ export enum Platform {
|
|||||||
|
|
||||||
export interface ConfigContext {
|
export interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
|
allowedAadEndpoints: ReadonlyArray<string>;
|
||||||
|
allowedGraphEndpoints: ReadonlyArray<string>;
|
||||||
allowedArmEndpoints: ReadonlyArray<string>;
|
allowedArmEndpoints: ReadonlyArray<string>;
|
||||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||||
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||||
@@ -37,10 +33,8 @@ export interface ConfigContext {
|
|||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
proxyPath?: string;
|
proxyPath?: string;
|
||||||
AAD_ENDPOINT: string;
|
AAD_ENDPOINT: string;
|
||||||
ARM_AUTH_AREA: string;
|
|
||||||
ARM_ENDPOINT: string;
|
ARM_ENDPOINT: string;
|
||||||
EMULATOR_ENDPOINT?: string;
|
EMULATOR_ENDPOINT?: string;
|
||||||
ARM_API_VERSION: string;
|
|
||||||
GRAPH_ENDPOINT: string;
|
GRAPH_ENDPOINT: string;
|
||||||
GRAPH_API_VERSION: string;
|
GRAPH_API_VERSION: string;
|
||||||
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
|
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
|
||||||
@@ -50,27 +44,24 @@ export interface ConfigContext {
|
|||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
PORTAL_BACKEND_ENDPOINT: string;
|
PORTAL_BACKEND_ENDPOINT: string;
|
||||||
NEW_BACKEND_APIS?: BackendApi[];
|
|
||||||
MONGO_PROXY_ENDPOINT: string;
|
MONGO_PROXY_ENDPOINT: string;
|
||||||
CASSANDRA_PROXY_ENDPOINT: string;
|
CASSANDRA_PROXY_ENDPOINT: string;
|
||||||
NEW_CASSANDRA_APIS?: string[];
|
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
GITHUB_CLIENT_ID: string;
|
GITHUB_CLIENT_ID: string;
|
||||||
GITHUB_TEST_ENV_CLIENT_ID: string;
|
GITHUB_TEST_ENV_CLIENT_ID: string;
|
||||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||||
isTerminalEnabled: boolean;
|
|
||||||
isPhoenixEnabled: boolean;
|
isPhoenixEnabled: boolean;
|
||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
msalRedirectURI?: string;
|
msalRedirectURI?: string;
|
||||||
globallyEnabledCassandraAPIs?: string[];
|
|
||||||
globallyEnabledMongoAPIs?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
let configContext: Readonly<ConfigContext> = {
|
let configContext: Readonly<ConfigContext> = {
|
||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
|
allowedAadEndpoints: defaultAllowedAadEndpoints,
|
||||||
|
allowedGraphEndpoints: defaultAllowedGraphEndpoints,
|
||||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||||
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||||
@@ -85,17 +76,12 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
||||||
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
||||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
|
||||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
|
||||||
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||||
ARM_AUTH_AREA: "https://management.azure.com/",
|
|
||||||
ARM_ENDPOINT: "https://management.azure.com/",
|
ARM_ENDPOINT: "https://management.azure.com/",
|
||||||
ARM_API_VERSION: "2016-06-01",
|
|
||||||
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
||||||
GRAPH_API_VERSION: "1.6",
|
GRAPH_API_VERSION: "1.6",
|
||||||
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
||||||
@@ -109,11 +95,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
|
||||||
isTerminalEnabled: false,
|
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
globallyEnabledCassandraAPIs: [],
|
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resetConfigContext(): void {
|
export function resetConfigContext(): void {
|
||||||
@@ -128,19 +110,38 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
if (newContext.allowedAadEndpoints) {
|
||||||
delete newContext.ARM_ENDPOINT;
|
Object.assign(configContext, { allowedAadEndpoints: newContext.allowedAadEndpoints });
|
||||||
|
}
|
||||||
|
if (newContext.allowedArmEndpoints) {
|
||||||
|
Object.assign(configContext, { allowedArmEndpoints: newContext.allowedArmEndpoints });
|
||||||
|
}
|
||||||
|
if (newContext.allowedGraphEndpoints) {
|
||||||
|
Object.assign(configContext, { allowedGraphEndpoints: newContext.allowedGraphEndpoints });
|
||||||
|
}
|
||||||
|
if (newContext.allowedBackendEndpoints) {
|
||||||
|
Object.assign(configContext, { allowedBackendEndpoints: newContext.allowedBackendEndpoints });
|
||||||
|
}
|
||||||
|
if (newContext.allowedMongoProxyEndpoints) {
|
||||||
|
Object.assign(configContext, { allowedMongoProxyEndpoints: newContext.allowedMongoProxyEndpoints });
|
||||||
|
}
|
||||||
|
if (newContext.allowedCassandraProxyEndpoints) {
|
||||||
|
Object.assign(configContext, { allowedCassandraProxyEndpoints: newContext.allowedCassandraProxyEndpoints });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
|
if (!validateEndpoint(newContext.AAD_ENDPOINT, configContext.allowedAadEndpoints)) {
|
||||||
delete newContext.AAD_ENDPOINT;
|
delete newContext.AAD_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints)) {
|
||||||
|
delete newContext.ARM_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
||||||
delete newContext.EMULATOR_ENDPOINT;
|
delete newContext.EMULATOR_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
|
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints)) {
|
||||||
delete newContext.GRAPH_ENDPOINT;
|
delete newContext.GRAPH_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,21 +149,15 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.ARCADIA_ENDPOINT;
|
delete newContext.ARCADIA_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!validateEndpoint(newContext.PORTAL_BACKEND_ENDPOINT, configContext.allowedBackendEndpoints)) {
|
||||||
!validateEndpoint(
|
delete newContext.PORTAL_BACKEND_ENDPOINT;
|
||||||
newContext.MONGO_PROXY_ENDPOINT,
|
}
|
||||||
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
|
|
||||||
)
|
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, configContext.allowedMongoProxyEndpoints)) {
|
||||||
) {
|
|
||||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, configContext.allowedCassandraProxyEndpoints)) {
|
||||||
!validateEndpoint(
|
|
||||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
|
||||||
configContext.allowedCassandraProxyEndpoints || defaultAllowedCassandraProxyEndpoints,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FabricMessageTypes } from "./FabricMessageTypes";
|
import { FabricMessageTypes } from "./FabricMessageTypes";
|
||||||
|
import { MessageTypes } from "./MessageTypes";
|
||||||
|
|
||||||
// This is the current version of these messages
|
// This is the current version of these messages
|
||||||
export const DATA_EXPLORER_RPC_VERSION = "3";
|
export const DATA_EXPLORER_RPC_VERSION = "3";
|
||||||
@@ -19,9 +20,32 @@ export type DataExploreMessageV3 =
|
|||||||
type: FabricMessageTypes.GetAllResourceTokens;
|
type: FabricMessageTypes.GetAllResourceTokens;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: FabricMessageTypes.GetAccessToken;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MessageTypes.TelemetryInfo;
|
||||||
|
data: {
|
||||||
|
action: string;
|
||||||
|
actionModifier: string;
|
||||||
|
data: unknown;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: FabricMessageTypes.OpenSettings;
|
type: FabricMessageTypes.OpenSettings;
|
||||||
settingsId: string;
|
params: [{ settingsId?: "About" | "Connection" }];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: FabricMessageTypes.RestoreContainer;
|
||||||
|
params: [];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: FabricMessageTypes.ContainerUpdated;
|
||||||
|
params: {
|
||||||
|
updateType: "created" | "deleted" | "settings";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export interface GetCosmosTokenMessageOptions {
|
export interface GetCosmosTokenMessageOptions {
|
||||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ export interface VectorEmbeddingPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorEmbedding {
|
export interface VectorEmbedding {
|
||||||
dataType: "float32" | "uint8" | "int8";
|
dataType: "float32" | "float16" | "uint8" | "int8";
|
||||||
dimensions: number;
|
dimensions: number;
|
||||||
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export enum FabricMessageTypes {
|
|||||||
GetAccessToken = "GetAccessToken",
|
GetAccessToken = "GetAccessToken",
|
||||||
Ready = "Ready",
|
Ready = "Ready",
|
||||||
OpenSettings = "OpenSettings",
|
OpenSettings = "OpenSettings",
|
||||||
|
RestoreContainer = "RestoreContainer",
|
||||||
|
ContainerUpdated = "ContainerUpdated",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthorizationToken {
|
export interface AuthorizationToken {
|
||||||
|
|||||||
@@ -443,6 +443,7 @@ export interface DataExplorerInputsFrame {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
feedbackPolicies?: any;
|
feedbackPolicies?: any;
|
||||||
|
aadToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelfServeFrameInputs {
|
export interface SelfServeFrameInputs {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html class="no-js" lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="data:," />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="heatmap"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
@import "../../../less/Common/Constants";
|
|
||||||
html {
|
|
||||||
font-family: @DataExplorerFont;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
border: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: @DataExplorerFont;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
border: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#heatmap {
|
|
||||||
.dark-theme {
|
|
||||||
color: @BaseLight;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chartTitle {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
left: 3px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noDataMessage {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10000;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
opacity: 0.97;
|
|
||||||
div {
|
|
||||||
border-color: rgba(204, 204, 204, 0.8);
|
|
||||||
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.12);
|
|
||||||
padding: 15px 10px;
|
|
||||||
width: calc(55% - 40px);
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: center;
|
|
||||||
border-width: 1px;
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { handleMessage, Heatmap, isDarkTheme } from "./Heatmap";
|
|
||||||
import { PortalTheme } from "./HeatmapDatatypes";
|
|
||||||
|
|
||||||
describe("The Heatmap Control", () => {
|
|
||||||
const dataPoints = {
|
|
||||||
"1": {
|
|
||||||
"2019-06-19T00:59:10Z": {
|
|
||||||
"Normalized Throughput": 0.35,
|
|
||||||
},
|
|
||||||
"2019-06-19T00:48:10Z": {
|
|
||||||
"Normalized Throughput": 0.25,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartCaptions = {
|
|
||||||
chartTitle: "chart title",
|
|
||||||
yAxisTitle: "YAxisTitle",
|
|
||||||
tooltipText: "Tooltip text",
|
|
||||||
timeWindow: 123456789,
|
|
||||||
};
|
|
||||||
|
|
||||||
let heatmap: Heatmap;
|
|
||||||
const theme: PortalTheme = 1;
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
|
||||||
|
|
||||||
describe("drawHeatmap rendering", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
heatmap = new Heatmap(dataPoints, chartCaptions, theme);
|
|
||||||
document.body.innerHTML = divElement;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
document.body.innerHTML = ``;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call _getChartSettings when drawHeatmap is invoked", () => {
|
|
||||||
const _getChartSettings = jest.spyOn(heatmap, "_getChartSettings");
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(_getChartSettings).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call _getLayoutSettings when drawHeatmap is invoked", () => {
|
|
||||||
const _getLayoutSettings = jest.spyOn(heatmap, "_getLayoutSettings");
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(_getLayoutSettings).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call _getChartDisplaySettings when drawHeatmap is invoked", () => {
|
|
||||||
const _getChartDisplaySettings = jest.spyOn(heatmap, "_getChartDisplaySettings");
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(_getChartDisplaySettings).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("drawHeatmap should render a Heatmap inside the div element", () => {
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(document.body.innerHTML).not.toEqual(divElement);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("generateMatrixFromMap", () => {
|
|
||||||
it("should massage input data to match output expected", () => {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).yAxisPoints).toEqual(["1"]);
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).dataPoints).toEqual([[0.25, 0.35]]);
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints.length).toEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should output the date format to ISO8601 string format", () => {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(10, 11)).toEqual("T");
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(-1)).toEqual("Z");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should convert the time to the user's local time", () => {
|
|
||||||
if (dayjs().utcOffset()) {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([
|
|
||||||
"2019-06-19T00:48:10Z",
|
|
||||||
"2019-06-19T00:59:10Z",
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([
|
|
||||||
"2019-06-19T00:48:10Z",
|
|
||||||
"2019-06-19T00:59:10Z",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isDarkTheme", () => {
|
|
||||||
it("isDarkTheme should return the correct result", () => {
|
|
||||||
expect(isDarkTheme(PortalTheme.dark)).toEqual(true);
|
|
||||||
expect(isDarkTheme(PortalTheme.azure)).not.toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("iframe rendering when there is no data", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
document.body.innerHTML = ``;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show a no data message with a dark theme", () => {
|
|
||||||
const data = {
|
|
||||||
data: {
|
|
||||||
signature: "pcIframe",
|
|
||||||
data: {
|
|
||||||
chartData: {},
|
|
||||||
chartSettings: {},
|
|
||||||
theme: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
origin: "http://localhost",
|
|
||||||
};
|
|
||||||
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
|
||||||
document.body.innerHTML = divElement;
|
|
||||||
|
|
||||||
handleMessage(data as MessageEvent);
|
|
||||||
expect(document.body.innerHTML).toContain("dark-theme");
|
|
||||||
expect(document.body.innerHTML).toContain("noDataMessage");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show a no data message with a white theme", () => {
|
|
||||||
const data = {
|
|
||||||
data: {
|
|
||||||
signature: "pcIframe",
|
|
||||||
data: {
|
|
||||||
chartData: {},
|
|
||||||
chartSettings: {},
|
|
||||||
theme: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
origin: "http://localhost",
|
|
||||||
};
|
|
||||||
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
|
||||||
document.body.innerHTML = divElement;
|
|
||||||
|
|
||||||
handleMessage(data as MessageEvent);
|
|
||||||
expect(document.body.innerHTML).not.toContain("dark-theme");
|
|
||||||
expect(document.body.innerHTML).toContain("noDataMessage");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import * as Plotly from "plotly.js-cartesian-dist-min";
|
|
||||||
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
|
|
||||||
import { StyleConstants } from "../../Common/StyleConstants";
|
|
||||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
|
||||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
|
||||||
import "./Heatmap.less";
|
|
||||||
import {
|
|
||||||
ChartSettings,
|
|
||||||
DataPayload,
|
|
||||||
DisplaySettings,
|
|
||||||
FontSettings,
|
|
||||||
HeatmapCaptions,
|
|
||||||
HeatmapData,
|
|
||||||
LayoutSettings,
|
|
||||||
PartitionTimeStampToData,
|
|
||||||
PortalTheme,
|
|
||||||
} from "./HeatmapDatatypes";
|
|
||||||
|
|
||||||
export class Heatmap {
|
|
||||||
public static readonly elementId: string = "heatmap";
|
|
||||||
|
|
||||||
private _chartData: HeatmapData;
|
|
||||||
private _heatmapCaptions: HeatmapCaptions;
|
|
||||||
private _theme: PortalTheme;
|
|
||||||
private _defaultFontColor: string;
|
|
||||||
|
|
||||||
constructor(data: DataPayload, heatmapCaptions: HeatmapCaptions, theme: PortalTheme) {
|
|
||||||
this._theme = theme;
|
|
||||||
this._defaultFontColor = StyleConstants.BaseDark;
|
|
||||||
this._setThemeColorForChart();
|
|
||||||
this._chartData = this.generateMatrixFromMap(data);
|
|
||||||
this._heatmapCaptions = heatmapCaptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setThemeColorForChart() {
|
|
||||||
if (isDarkTheme(this._theme)) {
|
|
||||||
this._defaultFontColor = StyleConstants.BaseLight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings {
|
|
||||||
return {
|
|
||||||
family: StyleConstants.DataExplorerFont,
|
|
||||||
size,
|
|
||||||
color,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public generateMatrixFromMap(data: DataPayload): HeatmapData {
|
|
||||||
// all keys in data payload, sorted...
|
|
||||||
const rows: string[] = Object.keys(data).sort((a: string, b: string) => {
|
|
||||||
if (parseInt(a) < parseInt(b)) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
if (parseInt(a) > parseInt(b)) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const output: HeatmapData = {
|
|
||||||
yAxisPoints: [],
|
|
||||||
dataPoints: [],
|
|
||||||
xAxisPoints: Object.keys(data[rows[0]]).sort((a: string, b: string) => {
|
|
||||||
if (a < b) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
if (a > b) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
// go thru all rows and create 2d matrix for heatmap...
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
|
||||||
output.yAxisPoints.push(rows[i]);
|
|
||||||
const dataPoints: number[] = [];
|
|
||||||
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
|
||||||
const row: PartitionTimeStampToData = data[rows[i]];
|
|
||||||
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
|
|
||||||
}
|
|
||||||
output.dataPoints.push(dataPoints);
|
|
||||||
}
|
|
||||||
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
|
||||||
const dateTime = output.xAxisPoints[a];
|
|
||||||
// convert to local users timezone...
|
|
||||||
const day = dayjs(new Date(dateTime)).format("YYYY-MM-DD");
|
|
||||||
const hour = dayjs(new Date(dateTime)).format("HH:mm:ss");
|
|
||||||
// coerce to ISOString format since that is what plotly wants...
|
|
||||||
output.xAxisPoints[a] = `${day}T${hour}Z`;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
// public for testing purposes
|
|
||||||
public _getChartSettings(): ChartSettings[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
z: this._chartData.dataPoints,
|
|
||||||
type: "heatmap",
|
|
||||||
zmin: 0,
|
|
||||||
zmid: 50,
|
|
||||||
zmax: 100,
|
|
||||||
colorscale: [
|
|
||||||
[0.0, "#1FD338"],
|
|
||||||
[0.1, "#1CAD2F"],
|
|
||||||
[0.2, "#50A527"],
|
|
||||||
[0.3, "#719F21"],
|
|
||||||
[0.4, "#95991B"],
|
|
||||||
[0.5, "#CE8F11"],
|
|
||||||
[0.6, "#E27F0F"],
|
|
||||||
[0.7, "#E46612"],
|
|
||||||
[0.8, "#E64914"],
|
|
||||||
[0.9, "#B80016"],
|
|
||||||
[1.0, "#B80016"],
|
|
||||||
],
|
|
||||||
name: "",
|
|
||||||
hovertemplate: this._heatmapCaptions.tooltipText,
|
|
||||||
colorbar: {
|
|
||||||
thickness: 15,
|
|
||||||
outlinewidth: 0,
|
|
||||||
tickcolor: StyleConstants.BaseDark,
|
|
||||||
tickfont: this._getFontStyles(10, this._defaultFontColor),
|
|
||||||
},
|
|
||||||
y: this._chartData.yAxisPoints,
|
|
||||||
x: this._chartData.xAxisPoints,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// public for testing purposes
|
|
||||||
public _getLayoutSettings(): LayoutSettings {
|
|
||||||
return {
|
|
||||||
margin: {
|
|
||||||
l: 40,
|
|
||||||
r: 10,
|
|
||||||
b: 35,
|
|
||||||
t: 30,
|
|
||||||
pad: 0,
|
|
||||||
},
|
|
||||||
paper_bgcolor: "transparent",
|
|
||||||
plot_bgcolor: "transparent",
|
|
||||||
width: 462,
|
|
||||||
height: 240,
|
|
||||||
yaxis: {
|
|
||||||
title: this._heatmapCaptions.yAxisTitle,
|
|
||||||
titlefont: this._getFontStyles(11),
|
|
||||||
autorange: true,
|
|
||||||
showgrid: false,
|
|
||||||
zeroline: false,
|
|
||||||
showline: false,
|
|
||||||
autotick: true,
|
|
||||||
fixedrange: true,
|
|
||||||
ticks: "",
|
|
||||||
showticklabels: false,
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
fixedrange: true,
|
|
||||||
title: "*White area in heatmap indicates there is no available data",
|
|
||||||
titlefont: this._getFontStyles(11),
|
|
||||||
autorange: true,
|
|
||||||
showgrid: false,
|
|
||||||
zeroline: false,
|
|
||||||
showline: false,
|
|
||||||
autotick: true,
|
|
||||||
tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e",
|
|
||||||
showticklabels: true,
|
|
||||||
tickfont: this._getFontStyles(10),
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: this._heatmapCaptions.chartTitle,
|
|
||||||
x: 0.01,
|
|
||||||
font: this._getFontStyles(13, this._defaultFontColor),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// public for testing purposes
|
|
||||||
public _getChartDisplaySettings(): DisplaySettings {
|
|
||||||
return {
|
|
||||||
/* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings
|
|
||||||
responsive: true,*/
|
|
||||||
displayModeBar: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public drawHeatmap(): void {
|
|
||||||
// todo - create random elementId generator so multiple heatmaps can be created - ticket # 431469
|
|
||||||
Plotly.plot(
|
|
||||||
Heatmap.elementId,
|
|
||||||
this._getChartSettings(),
|
|
||||||
this._getLayoutSettings(),
|
|
||||||
this._getChartDisplaySettings(),
|
|
||||||
);
|
|
||||||
const plotDiv: any = document.getElementById(Heatmap.elementId);
|
|
||||||
plotDiv.on("plotly_click", (data: any) => {
|
|
||||||
let timeSelected: string = data.points[0].x;
|
|
||||||
timeSelected = timeSelected.replace(" ", "T");
|
|
||||||
timeSelected = `${timeSelected}Z`;
|
|
||||||
let xAxisIndex = 0;
|
|
||||||
for (let i = 0; i < this._chartData.xAxisPoints.length; i++) {
|
|
||||||
if (this._chartData.xAxisPoints[i] === timeSelected) {
|
|
||||||
xAxisIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const output = [];
|
|
||||||
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
|
|
||||||
output.push(this._chartData.dataPoints[i][xAxisIndex]);
|
|
||||||
}
|
|
||||||
sendCachedDataMessage(MessageTypes.LogInfo, output);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDarkTheme(theme: PortalTheme) {
|
|
||||||
return theme === PortalTheme.dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleMessage(event: MessageEvent) {
|
|
||||||
if (isInvalidParentFrameOrigin(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof event.data.data !== "object" ||
|
|
||||||
!("chartData" in event.data.data) ||
|
|
||||||
!("chartSettings" in event.data.data)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Plotly.purge(Heatmap.elementId);
|
|
||||||
|
|
||||||
document.getElementById(Heatmap.elementId)!.innerHTML = "";
|
|
||||||
const data = event.data.data;
|
|
||||||
const chartData: DataPayload = data.chartData;
|
|
||||||
const chartSettings: HeatmapCaptions = data.chartSettings;
|
|
||||||
const chartTheme: PortalTheme = data.theme;
|
|
||||||
if (Object.keys(chartData).length) {
|
|
||||||
new Heatmap(chartData, chartSettings, chartTheme).drawHeatmap();
|
|
||||||
} else {
|
|
||||||
const chartTitleElement = document.createElement("div");
|
|
||||||
chartTitleElement.innerHTML = data.chartSettings.chartTitle;
|
|
||||||
chartTitleElement.classList.add("chartTitle");
|
|
||||||
|
|
||||||
const noDataMessageElement = document.createElement("div");
|
|
||||||
noDataMessageElement.classList.add("noDataMessage");
|
|
||||||
const noDataMessageContent = document.createElement("div");
|
|
||||||
noDataMessageContent.innerHTML = data.errorMessage;
|
|
||||||
|
|
||||||
noDataMessageElement.appendChild(noDataMessageContent);
|
|
||||||
|
|
||||||
if (isDarkTheme(chartTheme)) {
|
|
||||||
chartTitleElement.classList.add("dark-theme");
|
|
||||||
noDataMessageElement.classList.add("dark-theme");
|
|
||||||
noDataMessageContent.classList.add("dark-theme");
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement);
|
|
||||||
document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("message", handleMessage, false);
|
|
||||||
sendReadyMessage();
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
type dataPoint = string | number;
|
|
||||||
|
|
||||||
export interface DataPayload {
|
|
||||||
[id: string]: PartitionTimeStampToData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PortalTheme {
|
|
||||||
blue = 1,
|
|
||||||
azure,
|
|
||||||
light,
|
|
||||||
dark,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HeatmapData {
|
|
||||||
yAxisPoints: string[];
|
|
||||||
xAxisPoints: string[];
|
|
||||||
dataPoints: dataPoint[][];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HeatmapCaptions {
|
|
||||||
chartTitle: string;
|
|
||||||
yAxisTitle: string;
|
|
||||||
tooltipText: string;
|
|
||||||
timeWindow: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FontSettings {
|
|
||||||
family: string;
|
|
||||||
size: number;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LayoutSettings {
|
|
||||||
paper_bgcolor?: string;
|
|
||||||
plot_bgcolor?: string;
|
|
||||||
margin?: {
|
|
||||||
l: number;
|
|
||||||
r: number;
|
|
||||||
b: number;
|
|
||||||
t: number;
|
|
||||||
pad: number;
|
|
||||||
};
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
yaxis?: {
|
|
||||||
fixedrange: boolean;
|
|
||||||
title: HeatmapCaptions["yAxisTitle"];
|
|
||||||
titlefont: FontSettings;
|
|
||||||
autorange: boolean;
|
|
||||||
showgrid: boolean;
|
|
||||||
zeroline: boolean;
|
|
||||||
showline: boolean;
|
|
||||||
autotick: boolean;
|
|
||||||
ticks: "";
|
|
||||||
showticklabels: boolean;
|
|
||||||
};
|
|
||||||
xaxis?: {
|
|
||||||
fixedrange: boolean;
|
|
||||||
title: string;
|
|
||||||
titlefont: FontSettings;
|
|
||||||
autorange: boolean;
|
|
||||||
showgrid: boolean;
|
|
||||||
zeroline: boolean;
|
|
||||||
showline: boolean;
|
|
||||||
autotick: boolean;
|
|
||||||
showticklabels: boolean;
|
|
||||||
tickformat: string;
|
|
||||||
tickfont: FontSettings;
|
|
||||||
};
|
|
||||||
title?: {
|
|
||||||
text: HeatmapCaptions["chartTitle"];
|
|
||||||
x: number;
|
|
||||||
font?: FontSettings;
|
|
||||||
};
|
|
||||||
font?: FontSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChartSettings {
|
|
||||||
z: HeatmapData["dataPoints"];
|
|
||||||
type: "heatmap";
|
|
||||||
zmin: number;
|
|
||||||
zmid: number;
|
|
||||||
zmax: number;
|
|
||||||
colorscale: [number, string][];
|
|
||||||
name: string;
|
|
||||||
hovertemplate: HeatmapCaptions["tooltipText"];
|
|
||||||
colorbar: {
|
|
||||||
thickness: number;
|
|
||||||
outlinewidth: number;
|
|
||||||
tickcolor: string;
|
|
||||||
tickfont: FontSettings;
|
|
||||||
};
|
|
||||||
y: HeatmapData["yAxisPoints"];
|
|
||||||
x: HeatmapData["xAxisPoints"];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DisplaySettings {
|
|
||||||
displayModeBar: boolean;
|
|
||||||
responsive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PartitionTimeStampToData {
|
|
||||||
[timeSeriesDates: string]: {
|
|
||||||
[NormalizedThroughput: string]: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -22,6 +22,7 @@ export interface FullTextPoliciesComponentProps {
|
|||||||
) => void;
|
) => void;
|
||||||
discardChanges?: boolean;
|
discardChanges?: boolean;
|
||||||
onChangesDiscarded?: () => void;
|
onChangesDiscarded?: () => void;
|
||||||
|
englishOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FullTextPolicyData {
|
export interface FullTextPolicyData {
|
||||||
@@ -66,6 +67,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
|||||||
onFullTextPathChange,
|
onFullTextPathChange,
|
||||||
discardChanges,
|
discardChanges,
|
||||||
onChangesDiscarded,
|
onChangesDiscarded,
|
||||||
|
englishOnly,
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const getFullTextPathError = (path: string, index?: number): string => {
|
const getFullTextPathError = (path: string, index?: number): string => {
|
||||||
let error = "";
|
let error = "";
|
||||||
@@ -87,6 +89,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
|||||||
if (!fullTextPolicy) {
|
if (!fullTextPolicy) {
|
||||||
fullTextPolicy = { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] };
|
fullTextPolicy = { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullTextPolicy.fullTextPaths.map((fullTextPath: FullTextPath) => ({
|
return fullTextPolicy.fullTextPaths.map((fullTextPath: FullTextPath) => ({
|
||||||
...fullTextPath,
|
...fullTextPath,
|
||||||
pathError: getFullTextPathError(fullTextPath.path),
|
pathError: getFullTextPathError(fullTextPath.path),
|
||||||
@@ -166,7 +169,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
required={true}
|
required={true}
|
||||||
styles={dropdownStyles}
|
styles={dropdownStyles}
|
||||||
options={getFullTextLanguageOptions()}
|
options={getFullTextLanguageOptions(englishOnly)}
|
||||||
selectedKey={defaultLanguage}
|
selectedKey={defaultLanguage}
|
||||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
setDefaultLanguage(option.key as never)
|
setDefaultLanguage(option.key as never)
|
||||||
@@ -211,7 +214,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
required={true}
|
required={true}
|
||||||
styles={dropdownStyles}
|
styles={dropdownStyles}
|
||||||
options={getFullTextLanguageOptions()}
|
options={getFullTextLanguageOptions(englishOnly)}
|
||||||
selectedKey={fullTextPolicy.language}
|
selectedKey={fullTextPolicy.language}
|
||||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
onFullTextPathPolicyChange(index, option)
|
onFullTextPathPolicyChange(index, option)
|
||||||
@@ -229,11 +232,25 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFullTextLanguageOptions = (): IDropdownOption[] => {
|
export const getFullTextLanguageOptions = (englishOnly?: boolean): IDropdownOption[] => {
|
||||||
return [
|
const fullTextLanguageOptions: IDropdownOption[] = [
|
||||||
{
|
{
|
||||||
key: "en-US",
|
key: "en-US",
|
||||||
text: "English (US)",
|
text: "English (US)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "fr-FR",
|
||||||
|
text: "French",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "de-DE",
|
||||||
|
text: "German",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "es-ES",
|
||||||
|
text: "Spanish",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return englishOnly ? [fullTextLanguageOptions[0]] : fullTextLanguageOptions;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
||||||
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
|
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||||
import {
|
import {
|
||||||
ComputedPropertiesComponent,
|
ComputedPropertiesComponent,
|
||||||
ComputedPropertiesComponentProps,
|
ComputedPropertiesComponentProps,
|
||||||
@@ -431,6 +433,15 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.props.settingsTab.isExecuting(false);
|
this.props.settingsTab.isExecuting(false);
|
||||||
|
|
||||||
|
// Send message to Fabric no matter success or failure.
|
||||||
|
// In case of failure, saveCollectionSettings might have partially succeeded and Fabric needs to refresh
|
||||||
|
if (isFabricNative() && this.isCollectionSettingsTab) {
|
||||||
|
sendMessage({
|
||||||
|
type: FabricMessageTypes.ContainerUpdated,
|
||||||
|
params: { updateType: "settings" },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -559,26 +559,81 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
private getThroughputTextField = (): JSX.Element => (
|
private getThroughputTextField = (): JSX.Element => (
|
||||||
<>
|
<>
|
||||||
{this.props.isAutoPilotSelected ? (
|
{this.props.isAutoPilotSelected ? (
|
||||||
<TextField
|
<Stack horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
|
||||||
label="Maximum RU/s required by this resource"
|
{/* Column 1: Minimum RU/s */}
|
||||||
required
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
type="number"
|
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||||
id="autopilotInput"
|
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||||
key="auto pilot throughput input"
|
Minimum RU/s
|
||||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
</Text>
|
||||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
|
||||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
</Stack>
|
||||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
<Text
|
||||||
onChange={this.onAutoPilotThroughputChange}
|
style={{
|
||||||
min={autoPilotThroughput1K}
|
fontFamily: "Segoe UI",
|
||||||
onGetErrorMessage={(value: string) => {
|
width: 70,
|
||||||
const sanitizedValue = getSanitizedInputValue(value);
|
height: 28,
|
||||||
return sanitizedValue % 1000
|
border: "none",
|
||||||
? "Throughput value must be in increments of 1000"
|
fontSize: 14,
|
||||||
: this.props.throughputError;
|
backgroundColor: "transparent",
|
||||||
}}
|
fontWeight: 400,
|
||||||
validateOnLoad={false}
|
display: "flex",
|
||||||
/>
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Column 2: "x 10 =" Text */}
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: "Segoe UI",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
paddingBottom: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
x 10 =
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Column 3: Maximum RU/s */}
|
||||||
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
|
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||||
|
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||||
|
Maximum RU/s
|
||||||
|
</Text>
|
||||||
|
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
|
||||||
|
</Stack>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
id="autopilotInput"
|
||||||
|
key="auto pilot throughput input"
|
||||||
|
styles={{
|
||||||
|
...getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline),
|
||||||
|
fieldGroup: { width: 100, height: 28 },
|
||||||
|
field: { fontSize: 14, fontWeight: 400 },
|
||||||
|
}}
|
||||||
|
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||||
|
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||||
|
value={
|
||||||
|
this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()
|
||||||
|
}
|
||||||
|
onChange={this.onAutoPilotThroughputChange}
|
||||||
|
min={autoPilotThroughput1K}
|
||||||
|
onGetErrorMessage={(value: string) => {
|
||||||
|
const sanitizedValue = getSanitizedInputValue(value);
|
||||||
|
return sanitizedValue % 1000
|
||||||
|
? "Throughput value must be in increments of 1000"
|
||||||
|
: this.props.throughputError;
|
||||||
|
}}
|
||||||
|
validateOnLoad={false}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -157,35 +157,148 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StyledTextFieldBase
|
<Stack
|
||||||
disabled={true}
|
horizontal={true}
|
||||||
id="autopilotInput"
|
tokens={
|
||||||
key="auto pilot throughput input"
|
|
||||||
label="Maximum RU/s required by this resource"
|
|
||||||
min={1000}
|
|
||||||
onChange={[Function]}
|
|
||||||
onGetErrorMessage={[Function]}
|
|
||||||
required={true}
|
|
||||||
step={1000}
|
|
||||||
styles={
|
|
||||||
{
|
{
|
||||||
"fieldGroup": {
|
"childrenGap": 8,
|
||||||
"borderColor": "",
|
|
||||||
"height": 25,
|
|
||||||
"selectors": {
|
|
||||||
":disabled": {
|
|
||||||
"backgroundColor": undefined,
|
|
||||||
"borderColor": undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"width": 300,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type="number"
|
verticalAlign="end"
|
||||||
validateOnLoad={false}
|
>
|
||||||
value=""
|
<Stack
|
||||||
/>
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"fontWeight": 600,
|
||||||
|
"lineHeight": "20px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
Minimum RU/s
|
||||||
|
</Text>
|
||||||
|
<FontIcon
|
||||||
|
iconName="Info"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "#666",
|
||||||
|
"fontSize": 12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"alignItems": "center",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"border": "none",
|
||||||
|
"boxSizing": "border-box",
|
||||||
|
"display": "flex",
|
||||||
|
"fontFamily": "Segoe UI",
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": 400,
|
||||||
|
"height": 28,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"width": 70,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
400
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"fontFamily": "Segoe UI",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 400,
|
||||||
|
"paddingBottom": 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
x 10 =
|
||||||
|
</Text>
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"fontWeight": 600,
|
||||||
|
"lineHeight": "20px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
Maximum RU/s
|
||||||
|
</Text>
|
||||||
|
<FontIcon
|
||||||
|
iconName="Info"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "#666",
|
||||||
|
"fontSize": 12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
disabled={true}
|
||||||
|
id="autopilotInput"
|
||||||
|
key="auto pilot throughput input"
|
||||||
|
min={1000}
|
||||||
|
onChange={[Function]}
|
||||||
|
onGetErrorMessage={[Function]}
|
||||||
|
required={true}
|
||||||
|
step={1000}
|
||||||
|
styles={
|
||||||
|
{
|
||||||
|
"field": {
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": 400,
|
||||||
|
},
|
||||||
|
"fieldGroup": {
|
||||||
|
"height": 28,
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="number"
|
||||||
|
validateOnLoad={false}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ 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";
|
||||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||||
|
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
import * as SharedConstants from "../../../Shared/Constants";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||||
import "./ThroughputInput.less";
|
import "./ThroughputInput.less";
|
||||||
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
|
||||||
|
|
||||||
export interface ThroughputInputProps {
|
export interface ThroughputInputProps {
|
||||||
isDatabase: boolean;
|
isDatabase: boolean;
|
||||||
@@ -41,11 +41,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
let defaultThroughput: number;
|
let defaultThroughput: number;
|
||||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
const workloadType: Constants.WorkloadType = getWorkloadType();
|
||||||
|
|
||||||
if (
|
if (isFabricNative()) {
|
||||||
|
defaultThroughput = AutoPilotUtils.autoPilotThroughput5K;
|
||||||
|
} else if (
|
||||||
isFreeTier ||
|
isFreeTier ||
|
||||||
isQuickstart ||
|
isQuickstart ||
|
||||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType) ||
|
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
||||||
isFabricNative()
|
|
||||||
) {
|
) {
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
||||||
} else if (workloadType === Constants.WorkloadType.Production) {
|
} else if (workloadType === Constants.WorkloadType.Production) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IDropdownOption } from "@fluentui/react";
|
import { IDropdownOption } from "@fluentui/react";
|
||||||
|
|
||||||
const dataTypes = ["float32", "uint8", "int8"];
|
const dataTypes = ["float32", "float16", "uint8", "int8"];
|
||||||
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
||||||
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
|
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
|
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
|
||||||
@@ -90,12 +91,13 @@ export class ContainerSampleGenerator {
|
|||||||
}
|
}
|
||||||
const { databaseAccount: account } = userContext;
|
const { databaseAccount: account } = userContext;
|
||||||
const databaseId = collection.databaseId;
|
const databaseId = collection.databaseId;
|
||||||
|
|
||||||
const gremlinClient = new GremlinClient();
|
const gremlinClient = new GremlinClient();
|
||||||
gremlinClient.initialize({
|
gremlinClient.initialize({
|
||||||
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
|
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
|
||||||
databaseId: databaseId,
|
databaseId: databaseId,
|
||||||
collectionId: collection.id(),
|
collectionId: collection.id(),
|
||||||
masterKey: userContext.masterKey || "",
|
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||||
maxResultSize: 100,
|
maxResultSize: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ 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 {
|
||||||
|
isFabricMirrored,
|
||||||
|
isFabricMirroredKey,
|
||||||
|
isFabricNative,
|
||||||
|
scheduleRefreshFabricToken,
|
||||||
|
} 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";
|
||||||
@@ -284,14 +289,40 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openInVsCode(): void {
|
/**
|
||||||
|
* Generates a VS Code DocumentDB connection URL using the current user's MongoDB connection parameters.
|
||||||
|
* Double-encodes the updated connection string for safe usage in VS Code URLs.
|
||||||
|
*
|
||||||
|
* The DocumentDB VS Code extension requires double encoding for connection strings.
|
||||||
|
* See: https://microsoft.github.io/vscode-documentdb/manual/how-to-construct-url.html#double-encoding
|
||||||
|
*
|
||||||
|
* @returns {string} The encoded VS Code DocumentDB connection URL.
|
||||||
|
*/
|
||||||
|
private getDocumentDbUrl() {
|
||||||
|
const { adminLogin: adminLoginuserName = "", connectionString = "" } = userContext.vcoreMongoConnectionParams;
|
||||||
|
const updatedConnectionString = connectionString.replace(/<(user|username)>:<password>/i, adminLoginuserName);
|
||||||
|
const encodedUpdatedConnectionString = encodeURIComponent(encodeURIComponent(updatedConnectionString));
|
||||||
|
const documentDbUrl = `vscode://ms-azuretools.vscode-documentdb?connectionString=${encodedUpdatedConnectionString}`;
|
||||||
|
return documentDbUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCosmosDbUrl() {
|
||||||
const activeTab = useTabs.getState().activeTab;
|
const activeTab = useTabs.getState().activeTab;
|
||||||
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
|
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
|
||||||
const database = encodeURIComponent(activeTab?.collection?.databaseId);
|
const database = encodeURIComponent(activeTab?.collection?.databaseId);
|
||||||
const container = encodeURIComponent(activeTab?.collection?.id());
|
const container = encodeURIComponent(activeTab?.collection?.id());
|
||||||
const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
|
const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
|
||||||
const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
|
const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
|
||||||
|
return vscodeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVSCodeUrl(): string {
|
||||||
|
const isvCore = (userContext.apiType || userContext.databaseAccount.kind) === "VCoreMongo";
|
||||||
|
return isvCore ? this.getDocumentDbUrl() : this.getCosmosDbUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
public openInVsCode(): void {
|
||||||
|
const vscodeUrl = this.getVSCodeUrl();
|
||||||
const openVSCodeDialogProps: DialogProps = {
|
const openVSCodeDialogProps: DialogProps = {
|
||||||
linkProps: {
|
linkProps: {
|
||||||
linkText: "Download Visual Studio Code",
|
linkText: "Download Visual Studio Code",
|
||||||
@@ -1149,7 +1180,10 @@ export default class Explorer {
|
|||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
||||||
}
|
}
|
||||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
|
||||||
|
if (!isFabricNative()) {
|
||||||
|
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||||
const isNotebookEnabled =
|
const isNotebookEnabled =
|
||||||
@@ -1171,7 +1205,7 @@ export default class Explorer {
|
|||||||
await this.initNotebooks(userContext.databaseAccount);
|
await this.initNotebooks(userContext.databaseAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL") {
|
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()) {
|
||||||
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
|
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
|
||||||
updateUserContext({ throughputBucketsEnabled });
|
updateUserContext({ throughputBucketsEnabled });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,8 +163,7 @@ describe("GraphExplorer", () => {
|
|||||||
graphBackendEndpoint: "graphBackendEndpoint",
|
graphBackendEndpoint: "graphBackendEndpoint",
|
||||||
databaseId: "databaseId",
|
databaseId: "databaseId",
|
||||||
collectionId: "collectionId",
|
collectionId: "collectionId",
|
||||||
masterKey: "masterKey",
|
password: "password",
|
||||||
|
|
||||||
onLoadStartKey: 0,
|
onLoadStartKey: 0,
|
||||||
onLoadStartKeyChange: (newKey: number): void => {},
|
onLoadStartKeyChange: (newKey: number): void => {},
|
||||||
resourceId: "resourceId",
|
resourceId: "resourceId",
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export interface GraphExplorerProps {
|
|||||||
graphBackendEndpoint: string;
|
graphBackendEndpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
masterKey: string;
|
password: string;
|
||||||
|
|
||||||
onLoadStartKey: number;
|
onLoadStartKey: number;
|
||||||
onLoadStartKeyChange: (newKey: number) => void;
|
onLoadStartKeyChange: (newKey: number) => void;
|
||||||
@@ -1300,7 +1300,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
endpoint: `wss://${this.props.graphBackendEndpoint}`,
|
endpoint: `wss://${this.props.graphBackendEndpoint}`,
|
||||||
databaseId: this.props.databaseId,
|
databaseId: this.props.databaseId,
|
||||||
collectionId: this.props.collectionId,
|
collectionId: this.props.collectionId,
|
||||||
masterKey: this.props.masterKey,
|
password: this.props.password,
|
||||||
maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
|
maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,28 +8,28 @@ describe("Gremlin Client", () => {
|
|||||||
endpoint: null,
|
endpoint: null,
|
||||||
collectionId: null,
|
collectionId: null,
|
||||||
databaseId: null,
|
databaseId: null,
|
||||||
masterKey: null,
|
|
||||||
maxResultSize: 10000,
|
maxResultSize: 10000,
|
||||||
|
password: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should use databaseId, collectionId and masterKey to authenticate", () => {
|
it("should use databaseId, collectionId and password to authenticate", () => {
|
||||||
const collectionId = "collectionId";
|
const collectionId = "collectionId";
|
||||||
const databaseId = "databaseId";
|
const databaseId = "databaseId";
|
||||||
const masterKey = "masterKey";
|
const testPassword = "password";
|
||||||
const gremlinClient = new GremlinClient();
|
const gremlinClient = new GremlinClient();
|
||||||
|
|
||||||
gremlinClient.initialize({
|
gremlinClient.initialize({
|
||||||
endpoint: null,
|
endpoint: null,
|
||||||
collectionId,
|
collectionId,
|
||||||
databaseId,
|
databaseId,
|
||||||
masterKey,
|
|
||||||
maxResultSize: 0,
|
maxResultSize: 0,
|
||||||
|
password: testPassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
// User must includes these values
|
// User must includes these values
|
||||||
expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1);
|
expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1);
|
||||||
expect(gremlinClient.client.params.user.indexOf(databaseId)).not.toBe(-1);
|
expect(gremlinClient.client.params.user.indexOf(databaseId)).not.toBe(-1);
|
||||||
expect(gremlinClient.client.params.password).toEqual(masterKey);
|
expect(gremlinClient.client.params.password).toEqual(testPassword);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should aggregate RU charges across multiple responses", (done) => {
|
it("should aggregate RU charges across multiple responses", (done) => {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export interface GremlinClientParameters {
|
|||||||
endpoint: string;
|
endpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
masterKey: string;
|
|
||||||
maxResultSize: number;
|
maxResultSize: number;
|
||||||
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GremlinRequestResult {
|
export interface GremlinRequestResult {
|
||||||
@@ -43,7 +43,7 @@ export class GremlinClient {
|
|||||||
this.client = new GremlinSimpleClient({
|
this.client = new GremlinSimpleClient({
|
||||||
endpoint: params.endpoint,
|
endpoint: params.endpoint,
|
||||||
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
|
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
|
||||||
password: params.masterKey,
|
password: params.password,
|
||||||
successCallback: (result: Result) => {
|
successCallback: (result: Result) => {
|
||||||
this.storePendingResult(result);
|
this.storePendingResult(result);
|
||||||
this.flushResult(result.requestId);
|
this.flushResult(result.requestId);
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
import * as sinon from "sinon";
|
import * as sinon from "sinon";
|
||||||
import {
|
import {
|
||||||
|
GremlinRequestMessage,
|
||||||
|
GremlinResponseMessage,
|
||||||
GremlinSimpleClient,
|
GremlinSimpleClient,
|
||||||
GremlinSimpleClientParameters,
|
GremlinSimpleClientParameters,
|
||||||
Result,
|
Result,
|
||||||
GremlinRequestMessage,
|
|
||||||
GremlinResponseMessage,
|
|
||||||
} from "./GremlinSimpleClient";
|
} from "./GremlinSimpleClient";
|
||||||
|
|
||||||
describe("Gremlin Simple Client", () => {
|
describe("Gremlin Simple Client", () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
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 { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
@@ -30,7 +31,7 @@ export interface CommandBarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
||||||
contextButtons: [],
|
contextButtons: [] as CommandButtonComponentProps[],
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
||||||
@@ -43,6 +44,15 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||||
|
|
||||||
|
// Subscribe to the store changes that affect button creation
|
||||||
|
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||||
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
|
|
||||||
|
// Memoize the expensive button creation
|
||||||
|
const staticButtons = React.useMemo(() => {
|
||||||
|
return CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
|
||||||
|
}, [container, selectedNodeState, dataPlaneRbacEnabled, aadTokenUpdated]);
|
||||||
|
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
const buttons =
|
const buttons =
|
||||||
userContext.apiType === "Postgres"
|
userContext.apiType === "Postgres"
|
||||||
@@ -62,7 +72,6 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
|
|
||||||
const contextButtons = (buttons || []).concat(
|
const contextButtons = (buttons || []).concat(
|
||||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
|
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||||
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
||||||
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
||||||
@@ -68,15 +67,7 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
const loginButtonProps = createLoginForEntraIDButton(container);
|
||||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
|
||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const buttonProps = createLoginForEntraIDButton(container);
|
|
||||||
setLoginButtonProps(buttonProps);
|
|
||||||
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
|
|
||||||
|
|
||||||
if (loginButtonProps) {
|
if (loginButtonProps) {
|
||||||
addDivider();
|
addDivider();
|
||||||
buttons.push(loginButtonProps);
|
buttons.push(loginButtonProps);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
} 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 { DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT, 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";
|
||||||
@@ -60,7 +60,6 @@ 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";
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
|
||||||
|
|
||||||
export interface AddCollectionPanelProps {
|
export interface AddCollectionPanelProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
@@ -123,7 +122,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
isSharded: userContext.apiType !== "Tables",
|
isSharded: userContext.apiType !== "Tables",
|
||||||
partitionKey: getPartitionKey(props.isQuickstart),
|
partitionKey: getPartitionKey(props.isQuickstart),
|
||||||
subPartitionKeys: [],
|
subPartitionKeys: [],
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: isFabricNative(), // Dedicated throughput is only enabled in Fabric Native by default
|
||||||
createMongoWildCardIndex:
|
createMongoWildCardIndex:
|
||||||
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
||||||
useHashV1: false,
|
useHashV1: false,
|
||||||
@@ -406,9 +405,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
responsiveMode={999}
|
responsiveMode={999}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
|
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
|
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
|
||||||
@@ -448,8 +447,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
this.setState({ collectionId: event.target.value })
|
this.setState({ collectionId: event.target.value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
|
|
||||||
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
|
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
|
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
|
||||||
@@ -644,7 +644,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 } }}
|
||||||
@@ -708,7 +708,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.shouldShowCollectionThroughputInput() && (
|
{this.shouldShowCollectionThroughputInput() && !isFabricNative() && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={false}
|
isDatabase={false}
|
||||||
@@ -775,7 +775,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
|
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||||
|
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
|
||||||
|
)}
|
||||||
|
|
||||||
{shouldShowAnalyticalStoreOptions() && (
|
{shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
|
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
|
||||||
@@ -891,6 +893,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
) => {
|
) => {
|
||||||
this.setState({ fullTextPolicy, fullTextIndexes, fullTextPolicyValidated });
|
this.setState({ fullTextPolicy, fullTextIndexes, fullTextPolicyValidated });
|
||||||
}}
|
}}
|
||||||
|
englishOnly={true}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -1131,7 +1134,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
private shouldShowCollectionThroughputInput(): boolean {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (isFabricNative() || isServerlessAccount()) {
|
if (isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1352,8 +1355,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
// Throughput
|
// Throughput
|
||||||
if (isFabricNative()) {
|
if (isFabricNative()) {
|
||||||
// Fabric Native accounts are always autoscale and have a fixed throughput of 1K
|
autoPilotMaxThroughput = DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT;
|
||||||
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
|
||||||
offerThroughput = undefined;
|
offerThroughput = undefined;
|
||||||
} else if (databaseLevelThroughput) {
|
} else if (databaseLevelThroughput) {
|
||||||
if (this.state.createNewDatabase) {
|
if (this.state.createNewDatabase) {
|
||||||
|
|||||||
@@ -142,16 +142,16 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
<Separator
|
||||||
<Separator
|
className="panelSeparator"
|
||||||
className="panelSeparator"
|
style={
|
||||||
style={
|
{
|
||||||
{
|
"marginBottom": -4,
|
||||||
"marginBottom": -4,
|
"marginTop": -4,
|
||||||
"marginTop": -4,
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
@@ -202,16 +202,16 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</Stack>
|
<Separator
|
||||||
<Separator
|
className="panelSeparator"
|
||||||
className="panelSeparator"
|
style={
|
||||||
style={
|
{
|
||||||
{
|
"marginBottom": -5,
|
||||||
"marginBottom": -5,
|
"marginTop": -5,
|
||||||
"marginTop": -5,
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
|
|||||||
@@ -199,6 +199,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
|
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [mongoGuidRepresentation, setMongoGuidRepresentation] = useState<Constants.MongoGuidRepresentation>(
|
||||||
|
LocalStorageUtility.hasItem(StorageKey.MongoGuidRepresentation)
|
||||||
|
? (LocalStorageUtility.getEntryString(StorageKey.MongoGuidRepresentation) as Constants.MongoGuidRepresentation)
|
||||||
|
: Constants.MongoGuidRepresentation.CSharpLegacy,
|
||||||
|
);
|
||||||
|
const [ignorePartitionKeyOnDocumentUpdate, setIgnorePartitionKeyOnDocumentUpdate] = useState<boolean>(
|
||||||
|
LocalStorageUtility.getEntryBoolean(StorageKey.IgnorePartitionKeyOnDocumentUpdate),
|
||||||
|
);
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
const explorerVersion = configContext.gitSha;
|
const explorerVersion = configContext.gitSha;
|
||||||
@@ -261,6 +270,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||||
!isEmulator;
|
!isEmulator;
|
||||||
|
|
||||||
|
const shouldShowMongoGuidRepresentationOption = userContext.apiType === "Mongo";
|
||||||
|
|
||||||
const handlerOnSubmit = async () => {
|
const handlerOnSubmit = async () => {
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
@@ -412,6 +423,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldShowMongoGuidRepresentationOption) {
|
||||||
|
LocalStorageUtility.setEntryString(StorageKey.MongoGuidRepresentation, mongoGuidRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advanced settings
|
||||||
|
LocalStorageUtility.setEntryBoolean(
|
||||||
|
StorageKey.IgnorePartitionKeyOnDocumentUpdate,
|
||||||
|
ignorePartitionKeyOnDocumentUpdate,
|
||||||
|
);
|
||||||
|
|
||||||
setIsExecuting(false);
|
setIsExecuting(false);
|
||||||
logConsoleInfo(
|
logConsoleInfo(
|
||||||
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
|
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
|
||||||
@@ -433,9 +454,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldShowMongoGuidRepresentationOption) {
|
||||||
|
logConsoleInfo(
|
||||||
|
`Updated Mongo Guid Representation to ${LocalStorageUtility.getEntryString(
|
||||||
|
StorageKey.MongoGuidRepresentation,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logConsoleInfo(
|
logConsoleInfo(
|
||||||
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
`${ignorePartitionKeyOnDocumentUpdate ? "Enabled" : "Disabled"} ignoring partition key on document update`,
|
||||||
);
|
);
|
||||||
|
|
||||||
refreshExplorer && (await explorer.refreshExplorer());
|
refreshExplorer && (await explorer.refreshExplorer());
|
||||||
closeSidePanel();
|
closeSidePanel();
|
||||||
};
|
};
|
||||||
@@ -480,6 +510,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
|
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mongoGuidRepresentationDropdownOptions: IDropdownOption[] = [
|
||||||
|
{ key: Constants.MongoGuidRepresentation.CSharpLegacy, text: Constants.MongoGuidRepresentation.CSharpLegacy },
|
||||||
|
{ key: Constants.MongoGuidRepresentation.JavaLegacy, text: Constants.MongoGuidRepresentation.JavaLegacy },
|
||||||
|
{ key: Constants.MongoGuidRepresentation.PythonLegacy, text: Constants.MongoGuidRepresentation.PythonLegacy },
|
||||||
|
{ key: Constants.MongoGuidRepresentation.Standard, text: Constants.MongoGuidRepresentation.Standard },
|
||||||
|
];
|
||||||
|
|
||||||
const handleOnPriorityLevelOptionChange = (
|
const handleOnPriorityLevelOptionChange = (
|
||||||
ev: React.FormEvent<HTMLInputElement>,
|
ev: React.FormEvent<HTMLInputElement>,
|
||||||
option: IChoiceGroupOption,
|
option: IChoiceGroupOption,
|
||||||
@@ -562,6 +599,20 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
setRefreshExplorer(false);
|
setRefreshExplorer(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnMongoGuidRepresentationOptionChange = (
|
||||||
|
ev: React.FormEvent<HTMLInputElement>,
|
||||||
|
option: IDropdownOption,
|
||||||
|
): void => {
|
||||||
|
setMongoGuidRepresentation(option.key as Constants.MongoGuidRepresentation);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnIgnorePartitionKeyOnDocumentUpdateChange = (
|
||||||
|
ev: React.MouseEvent<HTMLElement>,
|
||||||
|
checked?: boolean,
|
||||||
|
): void => {
|
||||||
|
setIgnorePartitionKeyOnDocumentUpdate(!!checked);
|
||||||
|
};
|
||||||
|
|
||||||
const choiceButtonStyles = {
|
const choiceButtonStyles = {
|
||||||
root: {
|
root: {
|
||||||
clear: "both",
|
clear: "both",
|
||||||
@@ -1068,15 +1119,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
<div className={styles.settingsSectionContainer}>
|
<div className={styles.settingsSectionContainer}>
|
||||||
<div className={styles.settingsSectionDescription}>
|
<div className={styles.settingsSectionDescription}>
|
||||||
This is a sample database and collection with synthetic product data you can use to explore using
|
This is a sample database and collection with synthetic product data you can use to explore using
|
||||||
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and
|
NoSQL queries. This will appear as another database in the Data Explorer UI, and is created by,
|
||||||
is created by, and maintained by Microsoft at no cost to you.
|
and maintained by Microsoft at no cost to you.
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
styles={{
|
styles={{
|
||||||
label: { padding: 0 },
|
label: { padding: 0 },
|
||||||
}}
|
}}
|
||||||
className="padding"
|
className="padding"
|
||||||
ariaLabel="Enable sample db for Query Advisor"
|
ariaLabel="Enable sample db for query exploration"
|
||||||
checked={copilotSampleDBEnabled}
|
checked={copilotSampleDBEnabled}
|
||||||
onChange={handleSampleDatabaseChange}
|
onChange={handleSampleDatabaseChange}
|
||||||
label="Enable sample database"
|
label="Enable sample database"
|
||||||
@@ -1085,6 +1136,44 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
{shouldShowMongoGuidRepresentationOption && (
|
||||||
|
<AccordionItem value="14">
|
||||||
|
<AccordionHeader>
|
||||||
|
<div className={styles.header}>Guid Representation</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div className={styles.settingsSectionContainer}>
|
||||||
|
<div className={styles.settingsSectionDescription}>
|
||||||
|
GuidRepresentation in MongoDB refers to how Globally Unique Identifiers (GUIDs) are serialized and
|
||||||
|
deserialized when stored in BSON documents. This will apply to all document operations.
|
||||||
|
</div>
|
||||||
|
<Dropdown
|
||||||
|
aria-labelledby="mongoGuidRepresentation"
|
||||||
|
selectedKey={mongoGuidRepresentation}
|
||||||
|
options={mongoGuidRepresentationDropdownOptions}
|
||||||
|
onChange={handleOnMongoGuidRepresentationOptionChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
)}
|
||||||
|
<AccordionItem value="15">
|
||||||
|
<AccordionHeader>
|
||||||
|
<div className={styles.header}>Advanced Settings</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div className={styles.settingsSectionContainer}>
|
||||||
|
<Checkbox
|
||||||
|
styles={{ label: { padding: 0 } }}
|
||||||
|
className="padding"
|
||||||
|
ariaLabel="Ignore partition key on document update"
|
||||||
|
checked={ignorePartitionKeyOnDocumentUpdate}
|
||||||
|
onChange={handleOnIgnorePartitionKeyOnDocumentUpdateChange}
|
||||||
|
label="Ignore partition key on document update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -575,6 +575,37 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
<AccordionItem
|
||||||
|
value="15"
|
||||||
|
>
|
||||||
|
<AccordionHeader>
|
||||||
|
<div
|
||||||
|
className="___15c001r_0000000 fq02s40"
|
||||||
|
>
|
||||||
|
Advanced Settings
|
||||||
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
ariaLabel="Ignore partition key on document update"
|
||||||
|
checked={false}
|
||||||
|
className="padding"
|
||||||
|
label="Ignore partition key on document update"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
{
|
||||||
|
"label": {
|
||||||
|
"padding": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
@@ -838,6 +869,37 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
<AccordionItem
|
||||||
|
value="15"
|
||||||
|
>
|
||||||
|
<AccordionHeader>
|
||||||
|
<div
|
||||||
|
className="___15c001r_0000000 fq02s40"
|
||||||
|
>
|
||||||
|
Advanced Settings
|
||||||
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
ariaLabel="Ignore partition key on document update"
|
||||||
|
checked={false}
|
||||||
|
className="padding"
|
||||||
|
label="Ignore partition key on document update"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
{
|
||||||
|
"label": {
|
||||||
|
"padding": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { Stack } from "@fluentui/react";
|
import { Stack } from "@fluentui/react";
|
||||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
|
||||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
@@ -13,7 +11,6 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
|
|||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import SplitterLayout from "react-splitter-layout";
|
import SplitterLayout from "react-splitter-layout";
|
||||||
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
||||||
@@ -26,7 +23,8 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
||||||
readCopilotToggleStatus(userContext.databaseAccount),
|
readCopilotToggleStatus(userContext.databaseAccount),
|
||||||
);
|
);
|
||||||
const [tabActive, setTabActive] = useState<boolean>(true);
|
//TODO: Uncomment this useState when query copilot is reinstated in DE
|
||||||
|
// const [tabActive, setTabActive] = useState<boolean>(true);
|
||||||
|
|
||||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||||
@@ -70,17 +68,18 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||||
}, [query, selectedQuery, copilotActive]);
|
}, [query, selectedQuery, copilotActive]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
//TODO: Uncomment this effect when query copilot is reinstated in DE
|
||||||
return () => {
|
// React.useEffect(() => {
|
||||||
useTabs.subscribe((state: TabsState) => {
|
// return () => {
|
||||||
if (state.activeReactTab === ReactTabKind.QueryCopilot) {
|
// useTabs.subscribe((state: TabsState) => {
|
||||||
setTabActive(true);
|
// if (state.activeReactTab === ReactTabKind.QueryCopilot) {
|
||||||
} else {
|
// setTabActive(true);
|
||||||
setTabActive(false);
|
// } else {
|
||||||
}
|
// setTabActive(false);
|
||||||
});
|
// }
|
||||||
};
|
// });
|
||||||
}, []);
|
// };
|
||||||
|
// }, []);
|
||||||
|
|
||||||
const toggleCopilot = (toggle: boolean) => {
|
const toggleCopilot = (toggle: boolean) => {
|
||||||
setCopilotActive(toggle);
|
setCopilotActive(toggle);
|
||||||
@@ -90,6 +89,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
return (
|
return (
|
||||||
<Stack className="tab-pane" style={{ width: "100%" }}>
|
<Stack className="tab-pane" style={{ width: "100%" }}>
|
||||||
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
||||||
|
{/*TODO: Uncomment this section when query copilot is reinstated in DE
|
||||||
{tabActive && copilotActive && (
|
{tabActive && copilotActive && (
|
||||||
<QueryCopilotPromptbar
|
<QueryCopilotPromptbar
|
||||||
explorer={explorer}
|
explorer={explorer}
|
||||||
@@ -97,7 +97,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
databaseId={QueryCopilotSampleDatabaseId}
|
databaseId={QueryCopilotSampleDatabaseId}
|
||||||
containerId={QueryCopilotSampleContainerId}
|
containerId={QueryCopilotSampleContainerId}
|
||||||
></QueryCopilotPromptbar>
|
></QueryCopilotPromptbar>
|
||||||
)}
|
)} */}
|
||||||
<Stack className="tabPaneContentContainer">
|
<Stack className="tabPaneContentContainer">
|
||||||
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* Accordion top class
|
* Accordion top class
|
||||||
*/
|
*/
|
||||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
||||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
import { DocumentAddRegular, LinkMultipleRegular, OpenRegular } from "@fluentui/react-icons";
|
||||||
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
import { SampleDataConfiguration, SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
||||||
|
import { SampleDataFile } from "Explorer/SplashScreen/SampleUtil";
|
||||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||||
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
import AzureOpenAiIcon from "../../../images/AzureOpenAi.svg";
|
||||||
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
|
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
|
||||||
|
import GithubIcon from "../../../images/github-black-and-white.svg";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
|
||||||
export interface SplashScreenProps {
|
export interface SplashScreenProps {
|
||||||
@@ -26,11 +29,11 @@ const useStyles = makeStyles({
|
|||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
},
|
},
|
||||||
buttonsContainer: {
|
buttonsContainer: {
|
||||||
width: "584px",
|
width: "760px",
|
||||||
margin: "auto",
|
margin: "auto",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
padding: "16px",
|
padding: "16px",
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
gap: "10px",
|
gap: "10px",
|
||||||
gridAutoRows: "minmax(184px, auto)",
|
gridAutoRows: "minmax(184px, auto)",
|
||||||
},
|
},
|
||||||
@@ -53,6 +56,15 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
three: {
|
three: {
|
||||||
|
gridColumn: "4",
|
||||||
|
gridRow: "1",
|
||||||
|
"& img": {
|
||||||
|
width: "32px",
|
||||||
|
height: "32px",
|
||||||
|
margin: "auto",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
four: {
|
||||||
gridColumn: "3",
|
gridColumn: "3",
|
||||||
gridRow: "2",
|
gridRow: "2",
|
||||||
"& svg": {
|
"& svg": {
|
||||||
@@ -61,6 +73,15 @@ const useStyles = makeStyles({
|
|||||||
margin: "auto",
|
margin: "auto",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
five: {
|
||||||
|
gridColumn: "4",
|
||||||
|
gridRow: "2",
|
||||||
|
"& img": {
|
||||||
|
width: "32px",
|
||||||
|
height: "32px",
|
||||||
|
margin: "auto",
|
||||||
|
},
|
||||||
|
},
|
||||||
single: {
|
single: {
|
||||||
gridColumn: "1 / 4",
|
gridColumn: "1 / 4",
|
||||||
gridRow: "1 / 3",
|
gridRow: "1 / 3",
|
||||||
@@ -119,7 +140,7 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
|
|||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
return (
|
return (
|
||||||
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
|
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick} tabIndex={0}>
|
||||||
<div className={styles.buttonUpperPart}>{icon}</div>
|
<div className={styles.buttonUpperPart}>{icon}</div>
|
||||||
<div aria-label={title} className={styles.buttonLowerPart}>
|
<div aria-label={title} className={styles.buttonLowerPart}>
|
||||||
<div>{title}</div>
|
<div>{title}</div>
|
||||||
@@ -132,6 +153,8 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
|
|||||||
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
|
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [openSampleDataImportDialog, setOpenSampleDataImportDialog] = React.useState(false);
|
const [openSampleDataImportDialog, setOpenSampleDataImportDialog] = React.useState(false);
|
||||||
|
const [selectedSampleDataConfiguration, setSelectedSampleDataConfiguration] =
|
||||||
|
React.useState<SampleDataConfiguration>(undefined);
|
||||||
|
|
||||||
const getSplashScreenButtons = (): JSX.Element => {
|
const getSplashScreenButtons = (): JSX.Element => {
|
||||||
const buttons: FabricHomeScreenButtonProps[] = [
|
const buttons: FabricHomeScreenButtonProps[] = [
|
||||||
@@ -145,10 +168,30 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Sample data",
|
title: "Sample Data",
|
||||||
description: "Automatically load sample data in your database",
|
description: "Load sample data in your database",
|
||||||
icon: <img src={CosmosDbBlackIcon} />,
|
icon: <img src={CosmosDbBlackIcon} alt={"Azure Cosmos DB icon"} aria-hidden="true" />,
|
||||||
onClick: () => setOpenSampleDataImportDialog(true),
|
onClick: () => {
|
||||||
|
setSelectedSampleDataConfiguration({
|
||||||
|
databaseName: userContext.fabricContext?.databaseName,
|
||||||
|
newContainerName: "SampleData",
|
||||||
|
sampleDataFile: SampleDataFile.FABRIC_SAMPLE_DATA,
|
||||||
|
});
|
||||||
|
setOpenSampleDataImportDialog(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Sample Vector Data",
|
||||||
|
description: "Load sample vector data in your database",
|
||||||
|
icon: <img src={AzureOpenAiIcon} alt={"Azure Open AI icon"} aria-hidden="true" />,
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedSampleDataConfiguration({
|
||||||
|
databaseName: userContext.fabricContext?.databaseName,
|
||||||
|
newContainerName: "SampleVectorData",
|
||||||
|
sampleDataFile: SampleDataFile.FABRIC_SAMPLE_VECTOR_DATA,
|
||||||
|
});
|
||||||
|
setOpenSampleDataImportDialog(true);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "App development",
|
title: "App development",
|
||||||
@@ -156,17 +199,25 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
|||||||
icon: <LinkMultipleRegular />,
|
icon: <LinkMultipleRegular />,
|
||||||
onClick: () => window.open("https://aka.ms/cosmosdbfabricsdk", "_blank"),
|
onClick: () => window.open("https://aka.ms/cosmosdbfabricsdk", "_blank"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Sample Gallery",
|
||||||
|
description: "Get real-world end-to-end samples",
|
||||||
|
icon: <img src={GithubIcon} alt={"GitHub icon"} aria-hidden="true" />,
|
||||||
|
onClick: () => window.open("https://azurecosmosdb.github.io/gallery/?tags=example&tags=analytics", "_blank"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return isFabricNativeReadOnly() ? (
|
return isFabricNativeReadOnly() ? (
|
||||||
<div className={styles.buttonsContainer}>
|
<div className={styles.buttonsContainer}>
|
||||||
<FabricHomeScreenButton className={styles.single} {...buttons[2]} />
|
<FabricHomeScreenButton className={styles.single} {...buttons[3]} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.buttonsContainer}>
|
<div className={styles.buttonsContainer}>
|
||||||
<FabricHomeScreenButton className={styles.one} {...buttons[0]} />
|
<FabricHomeScreenButton className={styles.one} {...buttons[0]} />
|
||||||
<FabricHomeScreenButton className={styles.two} {...buttons[1]} />
|
<FabricHomeScreenButton className={styles.two} {...buttons[1]} />
|
||||||
<FabricHomeScreenButton className={styles.three} {...buttons[2]} />
|
<FabricHomeScreenButton className={styles.three} {...buttons[2]} />
|
||||||
|
<FabricHomeScreenButton className={styles.four} {...buttons[3]} />
|
||||||
|
<FabricHomeScreenButton className={styles.five} {...buttons[4]} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -179,18 +230,20 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
|||||||
open={openSampleDataImportDialog}
|
open={openSampleDataImportDialog}
|
||||||
setOpen={setOpenSampleDataImportDialog}
|
setOpen={setOpenSampleDataImportDialog}
|
||||||
explorer={props.explorer}
|
explorer={props.explorer}
|
||||||
databaseName={userContext.fabricContext?.databaseName}
|
sampleDataConfiguration={selectedSampleDataConfiguration}
|
||||||
/>
|
/>
|
||||||
<div className={styles.title} role="heading" aria-label={title}>
|
<div className={styles.title} role="heading" aria-label={title} aria-level={1}>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
{getSplashScreenButtons()}
|
{getSplashScreenButtons()}
|
||||||
{/* <div className={styles.footer}>
|
{
|
||||||
Need help?{" "}
|
<div className={styles.footer}>
|
||||||
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
|
Need help?{" "}
|
||||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
<Link href="https://learn.microsoft.com/fabric/database/cosmos-db/overview" target="_blank">
|
||||||
</Link>
|
Learn more <OpenRegular />
|
||||||
</div> */}
|
</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ import {
|
|||||||
tokens,
|
tokens,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { checkContainerExists, createContainer, importData } from "Explorer/SplashScreen/SampleUtil";
|
import { checkContainerExists, createContainer, importData, SampleDataFile } from "Explorer/SplashScreen/SampleUtil";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
|
||||||
const SAMPLE_DATA_CONTAINER_NAME = "SampleData";
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
dialogContent: {
|
dialogContent: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -24,6 +22,12 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface SampleDataConfiguration {
|
||||||
|
databaseName: string;
|
||||||
|
newContainerName: string;
|
||||||
|
sampleDataFile: SampleDataFile;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This dialog:
|
* This dialog:
|
||||||
* - creates a container
|
* - creates a container
|
||||||
@@ -35,11 +39,11 @@ export const SampleDataImportDialog: React.FC<{
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
databaseName: string;
|
sampleDataConfiguration: SampleDataConfiguration | undefined;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const [status, setStatus] = useState<"idle" | "creating" | "importing" | "completed" | "error">("idle");
|
const [status, setStatus] = useState<"idle" | "creating" | "importing" | "completed" | "error">("idle");
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const containerName = SAMPLE_DATA_CONTAINER_NAME;
|
const containerName = props.sampleDataConfiguration?.newContainerName;
|
||||||
const [collection, setCollection] = useState<ViewModels.Collection>(undefined);
|
const [collection, setCollection] = useState<ViewModels.Collection>(undefined);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
@@ -53,7 +57,7 @@ export const SampleDataImportDialog: React.FC<{
|
|||||||
|
|
||||||
const handleStartImport = async (): Promise<void> => {
|
const handleStartImport = async (): Promise<void> => {
|
||||||
setStatus("creating");
|
setStatus("creating");
|
||||||
const databaseName = props.databaseName;
|
const databaseName = props.sampleDataConfiguration.databaseName;
|
||||||
if (checkContainerExists(databaseName, containerName)) {
|
if (checkContainerExists(databaseName, containerName)) {
|
||||||
const msg = `The container "${containerName}" in database "${databaseName}" already exists. Please delete it and retry.`;
|
const msg = `The container "${containerName}" in database "${databaseName}" already exists. Please delete it and retry.`;
|
||||||
setStatus("error");
|
setStatus("error");
|
||||||
@@ -63,7 +67,12 @@ export const SampleDataImportDialog: React.FC<{
|
|||||||
|
|
||||||
let collection;
|
let collection;
|
||||||
try {
|
try {
|
||||||
collection = await createContainer(databaseName, containerName, props.explorer);
|
collection = await createContainer(
|
||||||
|
databaseName,
|
||||||
|
containerName,
|
||||||
|
props.explorer,
|
||||||
|
props.sampleDataConfiguration.sampleDataFile,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus("error");
|
setStatus("error");
|
||||||
setErrorMessage(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`);
|
setErrorMessage(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
@@ -72,7 +81,7 @@ export const SampleDataImportDialog: React.FC<{
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setStatus("importing");
|
setStatus("importing");
|
||||||
await importData(collection);
|
await importData(props.sampleDataConfiguration.sampleDataFile, collection);
|
||||||
setCollection(collection);
|
setCollection(collection);
|
||||||
setStatus("completed");
|
setStatus("completed");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import { JSONObject } from "@azure/cosmos";
|
||||||
import { BackendDefaults } from "Common/Constants";
|
import { BackendDefaults } from "Common/Constants";
|
||||||
import { createCollection } from "Common/dataAccess/createCollection";
|
import { createCollection } from "Common/dataAccess/createCollection";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import { DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
|
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public for unit tests
|
* Public for unit tests
|
||||||
@@ -26,12 +30,20 @@ const hasContainer = (
|
|||||||
export const checkContainerExists = (databaseName: string, containerName: string) =>
|
export const checkContainerExists = (databaseName: string, containerName: string) =>
|
||||||
hasContainer(databaseName, containerName, useDatabases.getState().databases);
|
hasContainer(databaseName, containerName, useDatabases.getState().databases);
|
||||||
|
|
||||||
|
export enum SampleDataFile {
|
||||||
|
COPILOT = "Copilot",
|
||||||
|
FABRIC_SAMPLE_DATA = "FabricSampleData",
|
||||||
|
FABRIC_SAMPLE_VECTOR_DATA = "FabricSampleVectorData",
|
||||||
|
}
|
||||||
|
|
||||||
export const createContainer = async (
|
export const createContainer = async (
|
||||||
databaseName: string,
|
databaseName: string,
|
||||||
containerName: string,
|
containerName: string,
|
||||||
explorer: Explorer,
|
explorer: Explorer,
|
||||||
|
sampleDataFile: SampleDataFile,
|
||||||
): Promise<ViewModels.Collection> => {
|
): Promise<ViewModels.Collection> => {
|
||||||
const createRequest: DataModels.CreateCollectionParams = {
|
const createRequest: DataModels.CreateCollectionParams = {
|
||||||
|
autoPilotMaxThroughput: isFabricNative() ? DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT : undefined,
|
||||||
createNewDatabase: false,
|
createNewDatabase: false,
|
||||||
collectionId: containerName,
|
collectionId: containerName,
|
||||||
databaseId: databaseName,
|
databaseId: databaseName,
|
||||||
@@ -41,6 +53,44 @@ export const createContainer = async (
|
|||||||
kind: "Hash",
|
kind: "Hash",
|
||||||
version: BackendDefaults.partitionKeyVersion,
|
version: BackendDefaults.partitionKeyVersion,
|
||||||
},
|
},
|
||||||
|
vectorEmbeddingPolicy:
|
||||||
|
sampleDataFile === SampleDataFile.FABRIC_SAMPLE_VECTOR_DATA
|
||||||
|
? {
|
||||||
|
vectorEmbeddings: [
|
||||||
|
{
|
||||||
|
path: "/descriptionVector",
|
||||||
|
dataType: "float32",
|
||||||
|
distanceFunction: "cosine",
|
||||||
|
dimensions: 512,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
indexingPolicy:
|
||||||
|
sampleDataFile === SampleDataFile.FABRIC_SAMPLE_VECTOR_DATA
|
||||||
|
? {
|
||||||
|
automatic: true,
|
||||||
|
indexingMode: "consistent",
|
||||||
|
includedPaths: [
|
||||||
|
{
|
||||||
|
path: "/*",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
excludedPaths: [
|
||||||
|
{
|
||||||
|
path: '/"_etag"/?',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fullTextIndexes: [],
|
||||||
|
vectorIndexes: [
|
||||||
|
{
|
||||||
|
path: "/descriptionVector",
|
||||||
|
type: "quantizedFlat",
|
||||||
|
quantizationByteSize: 64,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
await createCollection(createRequest);
|
await createCollection(createRequest);
|
||||||
await explorer.refreshAllDatabases();
|
await explorer.refreshAllDatabases();
|
||||||
@@ -55,10 +105,39 @@ export const createContainer = async (
|
|||||||
|
|
||||||
const SAMPLE_DATA_PARTITION_KEY = "category"; // This pkey is specifically set for queryCopilotSampleData.json below
|
const SAMPLE_DATA_PARTITION_KEY = "category"; // This pkey is specifically set for queryCopilotSampleData.json below
|
||||||
|
|
||||||
export const importData = async (collection: ViewModels.Collection): Promise<void> => {
|
export const importData = async (sampleDataFile: SampleDataFile, collection: ViewModels.Collection): Promise<void> => {
|
||||||
// TODO: keep same chunk as ContainerSampleGenerator
|
let documents: JSONObject[] = undefined;
|
||||||
const dataFileContent = await import(
|
switch (sampleDataFile) {
|
||||||
/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json"
|
case SampleDataFile.COPILOT:
|
||||||
);
|
documents = (
|
||||||
await collection.bulkInsertDocuments(dataFileContent.data);
|
await import(/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json")
|
||||||
|
).data;
|
||||||
|
break;
|
||||||
|
case SampleDataFile.FABRIC_SAMPLE_DATA:
|
||||||
|
documents = (await import(/* webpackChunkName: "fabricSampleData" */ "../../../sampleData/fabricSampleData.json"))
|
||||||
|
.default;
|
||||||
|
break;
|
||||||
|
case SampleDataFile.FABRIC_SAMPLE_VECTOR_DATA:
|
||||||
|
documents = (
|
||||||
|
await import(
|
||||||
|
/* webpackChunkName: "fabricSampleDataVectors" */ "../../../sampleData/fabricSampleDataVectors.json"
|
||||||
|
)
|
||||||
|
).default;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown sample data file: ${sampleDataFile}`);
|
||||||
|
}
|
||||||
|
if (!documents) {
|
||||||
|
throw new Error(`Failed to load sample data file: ${sampleDataFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time it
|
||||||
|
const start = performance.now();
|
||||||
|
await collection.bulkInsertDocuments(documents);
|
||||||
|
const end = performance.now();
|
||||||
|
TelemetryProcessor.trace(Action.ImportSampleData, ActionModifiers.Success, {
|
||||||
|
documentsCount: documents.length,
|
||||||
|
durationMs: end - start,
|
||||||
|
sampleDataFile,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { ReactTabKind, useTabs } from "hooks/useTabs";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||||
import ContainersIcon from "../../../images/Containers.svg";
|
import ContainersIcon from "../../../images/Containers.svg";
|
||||||
|
import CosmosDBIcon from "../../../images/CosmosDB-logo.svg";
|
||||||
import LinkIcon from "../../../images/Link_blue.svg";
|
import LinkIcon from "../../../images/Link_blue.svg";
|
||||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||||
@@ -120,11 +121,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getSplashScreenButtons = (): JSX.Element => {
|
private getSplashScreenButtons = (): JSX.Element => {
|
||||||
if (
|
if (userContext.apiType === "SQL") {
|
||||||
userContext.apiType === "SQL" &&
|
|
||||||
useQueryCopilot.getState().copilotEnabled &&
|
|
||||||
useDatabases.getState().sampleDataResourceTokenCollection
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className="splashStackContainer"
|
className="splashStackContainer"
|
||||||
@@ -152,25 +149,18 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack className="splashStackRow" horizontal>
|
<Stack className="splashStackRow" horizontal>
|
||||||
{useQueryCopilot.getState().copilotEnabled && (
|
<SplashScreenButton
|
||||||
<SplashScreenButton
|
imgSrc={CosmosDBIcon}
|
||||||
imgSrc={CopilotIcon}
|
imgSize={35}
|
||||||
title={"Query faster with Query Advisor"}
|
title={"Azure Cosmos DB Samples Gallery"}
|
||||||
description={
|
description={
|
||||||
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
"Discover samples that showcase scalable, intelligent app patterns. Try one now to see how fast you can go from concept to code with Cosmos DB"
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const copilotVersion = userContext.features.copilotVersion;
|
window.open("https://azurecosmosdb.github.io/gallery/?tags=example", "_blank");
|
||||||
if (copilotVersion === "v1.0") {
|
traceOpen(Action.LearningResourcesClicked, { apiType: userContext.apiType });
|
||||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
}}
|
||||||
} else if (copilotVersion === "v2.0") {
|
/>
|
||||||
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
|
|
||||||
sampleCollection.onNewQueryClick(sampleCollection, undefined);
|
|
||||||
}
|
|
||||||
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<SplashScreenButton
|
<SplashScreenButton
|
||||||
imgSrc={ConnectIcon}
|
imgSrc={ConnectIcon}
|
||||||
title={"Connect"}
|
title={"Connect"}
|
||||||
@@ -212,6 +202,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
sample data, query.
|
sample data, query.
|
||||||
</TeachingBubble>
|
</TeachingBubble>
|
||||||
)}
|
)}
|
||||||
|
{/*TODO: convert below to use SplashScreenButton */}
|
||||||
{mainItems.map((item) => (
|
{mainItems.map((item) => (
|
||||||
<Stack
|
<Stack
|
||||||
id={`mainButton-${item.id}`}
|
id={`mainButton-${item.id}`}
|
||||||
@@ -477,6 +468,34 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Re-enable lint rule when query copilot is reinstated in DE
|
||||||
|
/* eslint-disable-next-line no-unused-vars */
|
||||||
|
private getQueryCopilotCard = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{useQueryCopilot.getState().copilotEnabled && (
|
||||||
|
<SplashScreenButton
|
||||||
|
imgSrc={CopilotIcon}
|
||||||
|
title={"Query faster with Query Advisor"}
|
||||||
|
description={
|
||||||
|
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
const copilotVersion = userContext.features.copilotVersion;
|
||||||
|
if (copilotVersion === "v1.0") {
|
||||||
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||||
|
} else if (copilotVersion === "v2.0") {
|
||||||
|
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
|
||||||
|
sampleCollection.onNewQueryClick(sampleCollection, undefined);
|
||||||
|
}
|
||||||
|
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
||||||
return {
|
return {
|
||||||
iconSrc: CollectionIcon,
|
iconSrc: CollectionIcon,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface SplashScreenButtonProps {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
imgSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||||
@@ -14,6 +15,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
onClick,
|
onClick,
|
||||||
|
imgSize,
|
||||||
}: SplashScreenButtonProps): JSX.Element => {
|
}: SplashScreenButtonProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
@@ -39,7 +41,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
|||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<img src={imgSrc} alt={title} aria-hidden="true" />
|
<img src={imgSrc} alt={title} aria-hidden="true" {...(imgSize ? { height: imgSize, width: imgSize } : {})} />
|
||||||
</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>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
|||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader, isDataplaneRbacEnabledForProxyApi } from "../../Utils/AuthorizationUtils";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
@@ -551,6 +551,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||||
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
|
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
|
||||||
|
|
||||||
|
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||||
|
xhr.setRequestHeader(Constants.HttpHeaders.entraIdToken, userContext.aadToken);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { formatErrorMessage, formatInfoMessage, formatWarningMessage } from "./U
|
|||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const DEFAULT_CLOUDSHELL_REGION = "westus";
|
const DEFAULT_CLOUDSHELL_REGION = "westus";
|
||||||
|
const DEFAULT_FAIRFAX_CLOUDSHELL_REGION = "usgovvirginia";
|
||||||
const POLLING_INTERVAL_MS = 2000;
|
const POLLING_INTERVAL_MS = 2000;
|
||||||
const MAX_RETRY_COUNT = 10;
|
const MAX_RETRY_COUNT = 10;
|
||||||
const MAX_PING_COUNT = 120 * 60; // 120 minutes (60 seconds/minute)
|
const MAX_PING_COUNT = 120 * 60; // 120 minutes (60 seconds/minute)
|
||||||
@@ -44,32 +45,26 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
|
|||||||
|
|
||||||
resolvedRegion = determineCloudShellRegion();
|
resolvedRegion = determineCloudShellRegion();
|
||||||
|
|
||||||
resolvedRegion = determineCloudShellRegion();
|
|
||||||
|
|
||||||
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
|
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
|
||||||
terminal.writeln(
|
terminal.writeln(
|
||||||
formatInfoMessage(
|
formatInfoMessage(
|
||||||
"The Cloud Shell environment will operate in a region that may differ from your database's region.",
|
"The Cloud Shell environment will operate in a region that may differ from your database's region.",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
terminal.writeln(formatInfoMessage("This has two potential implications:"));
|
terminal.writeln(formatInfoMessage("By using this feature, you acknowledge and agree to the following"));
|
||||||
terminal.writeln(formatInfoMessage("1. Performance Impact:"));
|
terminal.writeln(formatInfoMessage("1. Performance Impact:"));
|
||||||
terminal.writeln(
|
terminal.writeln(
|
||||||
formatInfoMessage(" Commands may experience higher latency due to geographic distance between regions."),
|
formatInfoMessage(" Commands may experience higher latency due to geographic distance between regions."),
|
||||||
);
|
);
|
||||||
terminal.writeln(formatInfoMessage("2. Data Compliance Considerations:"));
|
terminal.writeln(formatInfoMessage("2. Data Transfers:"));
|
||||||
terminal.writeln(
|
terminal.writeln(
|
||||||
formatInfoMessage(
|
formatInfoMessage(
|
||||||
" Data processed through this shell could temporarily reside in a different geographic region,",
|
" Data processed through this Cloud Shell service can be processed outside of your tenant's geographical region, compliance boundary or national cloud instance.",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
terminal.writeln(
|
|
||||||
formatInfoMessage(" which may affect compliance with data residency requirements or regulations specific"),
|
|
||||||
);
|
|
||||||
terminal.writeln(formatInfoMessage(" to your organization."));
|
|
||||||
terminal.writeln("");
|
terminal.writeln("");
|
||||||
|
|
||||||
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data governance and compliance, please visit:");
|
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data residency, please visit:");
|
||||||
terminal.writeln("\x1b[94mhttps://learn.microsoft.com/en-us/azure/cosmos-db/data-residency\x1b[0m");
|
terminal.writeln("\x1b[94mhttps://learn.microsoft.com/en-us/azure/cosmos-db/data-residency\x1b[0m");
|
||||||
|
|
||||||
// Ask for user consent for region
|
// Ask for user consent for region
|
||||||
@@ -159,7 +154,9 @@ export const ensureCloudShellProviderRegistered = async (): Promise<void> => {
|
|||||||
* Determines the appropriate CloudShell region
|
* Determines the appropriate CloudShell region
|
||||||
*/
|
*/
|
||||||
export const determineCloudShellRegion = (): string => {
|
export const determineCloudShellRegion = (): string => {
|
||||||
return getNormalizedRegion(userContext.databaseAccount?.location, DEFAULT_CLOUDSHELL_REGION);
|
const defaultRegion =
|
||||||
|
userContext.portalEnv === "fairfax" ? DEFAULT_FAIRFAX_CLOUDSHELL_REGION : DEFAULT_CLOUDSHELL_REGION;
|
||||||
|
return getNormalizedRegion(userContext.databaseAccount?.location, defaultRegion);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -258,14 +258,7 @@ Key limitations:
|
|||||||
|
|
||||||
### Data Residency
|
### Data Residency
|
||||||
|
|
||||||
Data residency requirements may not be fully satisfied when using CloudShell due to limited regional availability. CloudShell services are currently available in the following regions:
|
Data residency requirements may not be fully satisfied when using CloudShell due to limited regional availability.
|
||||||
|
|
||||||
| Geography | Regions |
|
|
||||||
|-----------|---------|
|
|
||||||
| Americas | East US, West US 2, South Central US, West Central US |
|
|
||||||
| Europe | West Europe, North Europe |
|
|
||||||
| Asia Pacific | Southeast Asia, Japan East, Australia East |
|
|
||||||
| Middle East | UAE North |
|
|
||||||
|
|
||||||
**Note:** For up-to-date supported regions, refer to the region configuration in:
|
**Note:** For up-to-date supported regions, refer to the region configuration in:
|
||||||
`src/Explorer/CloudShell/Configuration/RegionConfig.ts`
|
`src/Explorer/CloudShell/Configuration/RegionConfig.ts`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AbstractShellHandler, DISABLE_HISTORY, START_MARKER, EXIT_COMMAND } from "./AbstractShellHandler";
|
import { AbstractShellHandler, DISABLE_HISTORY, EXIT_COMMAND, START_MARKER } from "./AbstractShellHandler";
|
||||||
|
|
||||||
// Mock implementation for testing
|
// Mock implementation for testing
|
||||||
class MockShellHandler extends AbstractShellHandler {
|
class MockShellHandler extends AbstractShellHandler {
|
||||||
@@ -18,8 +18,8 @@ class MockShellHandler extends AbstractShellHandler {
|
|||||||
return "mock-endpoint";
|
return "mock-endpoint";
|
||||||
}
|
}
|
||||||
|
|
||||||
getTerminalSuppressedData(): string {
|
getTerminalSuppressedData(): string[] {
|
||||||
return "suppressed-data";
|
return ["suppressed-data"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ describe("AbstractShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return the terminal suppressed data", () => {
|
it("should return the terminal suppressed data", () => {
|
||||||
expect(shellHandler.getTerminalSuppressedData()).toBe("suppressed-data");
|
expect(shellHandler.getTerminalSuppressedData()).toEqual(["suppressed-data"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,18 @@ export const DISABLE_HISTORY = `set +o history`;
|
|||||||
* Command that displays an error message and exits the shell session.
|
* Command that displays an error message and exits the shell session.
|
||||||
* Used when shell initialization or connection fails.
|
* Used when shell initialization or connection fails.
|
||||||
*/
|
*/
|
||||||
export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && exit`;
|
export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && disown -a && exit`;
|
||||||
|
/**
|
||||||
|
* Command that displays error message with MongoDB networking guidance and exits the shell session.
|
||||||
|
* Used when MongoDB shell connection fails due to networking issues.
|
||||||
|
*/
|
||||||
|
export const EXIT_COMMAND_MONGO = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && printf "\\033[1;36mPlease use the 'Add Azure Cloud Shell IPs' button in the Networking blade to allow Cloud Shell access, if not already configured.\\033[0m\\n" && disown -a && exit`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This command runs mongosh in no-database and quiet mode,
|
||||||
|
* and evaluates the `disableTelemetry()` function to turn off telemetry collection.
|
||||||
|
*/
|
||||||
|
export const DISABLE_TELEMETRY_COMMAND = `mongosh --nodb --quiet --eval 'disableTelemetry()'`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class that defines the interface for shell-specific handlers
|
* Abstract class that defines the interface for shell-specific handlers
|
||||||
@@ -31,7 +42,16 @@ export abstract class AbstractShellHandler {
|
|||||||
abstract getShellName(): string;
|
abstract getShellName(): string;
|
||||||
abstract getSetUpCommands(): string[];
|
abstract getSetUpCommands(): string[];
|
||||||
abstract getConnectionCommand(): string;
|
abstract getConnectionCommand(): string;
|
||||||
abstract getTerminalSuppressedData(): string;
|
abstract getTerminalSuppressedData(): string[];
|
||||||
|
updateTerminalData?(data: string): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the exit command to use when connection fails.
|
||||||
|
* Can be overridden by subclasses to provide custom exit commands.
|
||||||
|
*/
|
||||||
|
protected getExitCommand(): string {
|
||||||
|
return EXIT_COMMAND;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs the complete initialization command sequence for the shell.
|
* Constructs the complete initialization command sequence for the shell.
|
||||||
@@ -57,7 +77,7 @@ export abstract class AbstractShellHandler {
|
|||||||
START_MARKER,
|
START_MARKER,
|
||||||
DISABLE_HISTORY,
|
DISABLE_HISTORY,
|
||||||
...setupCommands,
|
...setupCommands,
|
||||||
`{ ${connectionCommand}; } || true;${EXIT_COMMAND}`,
|
`{ ${connectionCommand}; } || true;${this.getExitCommand()}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
return allCommands.join("\n").concat("\n");
|
return allCommands.join("\n").concat("\n");
|
||||||
@@ -77,7 +97,7 @@ export abstract class AbstractShellHandler {
|
|||||||
* is not already present in the environment.
|
* is not already present in the environment.
|
||||||
*/
|
*/
|
||||||
protected mongoShellSetupCommands(): string[] {
|
protected mongoShellSetupCommands(): string[] {
|
||||||
const PACKAGE_VERSION: string = "2.5.0";
|
const PACKAGE_VERSION: string = "2.5.6";
|
||||||
return [
|
return [
|
||||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
||||||
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||||
@@ -85,7 +105,7 @@ export abstract class AbstractShellHandler {
|
|||||||
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
|
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
|
||||||
`if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
`if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
||||||
"source ~/.bashrc",
|
"if ! command -v mongosh &> /dev/null; then source ~/.bashrc; fi",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ describe("CassandraShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should return the correct terminal suppressed data", () => {
|
test("should return the correct terminal suppressed data", () => {
|
||||||
expect(handler.getTerminalSuppressedData()).toBe("");
|
expect(handler.getTerminalSuppressedData()).toEqual([""]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should include the correct package version in setup commands", () => {
|
test("should include the correct package version in setup commands", () => {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class CassandraShellHandler extends AbstractShellHandler {
|
|||||||
return `cqlsh ${getHostFromUrl(this._endpoint)} 10350 -u ${dbName} -p ${this._key} --ssl`;
|
return `cqlsh ${getHostFromUrl(this._endpoint)} 10350 -u ${dbName} -p ${this._key} --ssl`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "";
|
return [""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ interface DatabaseAccount {
|
|||||||
|
|
||||||
interface UserContextType {
|
interface UserContextType {
|
||||||
databaseAccount: DatabaseAccount;
|
databaseAccount: DatabaseAccount;
|
||||||
|
features: {
|
||||||
|
enableAadDataPlane: boolean;
|
||||||
|
};
|
||||||
|
apiType: string;
|
||||||
|
dataPlaneRbacEnabled: boolean;
|
||||||
|
aadToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
@@ -29,10 +35,13 @@ jest.mock("../../../../UserContext", () => ({
|
|||||||
mongoEndpoint: "https://test-mongo.documents.azure.com:443/",
|
mongoEndpoint: "https://test-mongo.documents.azure.com:443/",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
features: { enableAadDataPlane: false },
|
||||||
|
apiType: "Mongo",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../Utils/CommonUtils", () => ({
|
jest.mock("../Utils/CommonUtils", () => ({
|
||||||
|
...jest.requireActual("../Utils/CommonUtils"),
|
||||||
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
|
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -69,7 +78,7 @@ describe("MongoShellHandler", () => {
|
|||||||
|
|
||||||
expect(Array.isArray(commands)).toBe(true);
|
expect(Array.isArray(commands)).toBe(true);
|
||||||
expect(commands.length).toBe(7);
|
expect(commands.length).toBe(7);
|
||||||
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,11 +96,12 @@ describe("MongoShellHandler", () => {
|
|||||||
kind: "test-kind",
|
kind: "test-kind",
|
||||||
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
|
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
|
||||||
};
|
};
|
||||||
|
(userContext as UserContextType).dataPlaneRbacEnabled = false;
|
||||||
|
|
||||||
const command = mongoShellHandler.getConnectionCommand();
|
const command = mongoShellHandler.getConnectionCommand();
|
||||||
|
|
||||||
expect(command).toBe(
|
expect(command).toBe(
|
||||||
"mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
|
"mongosh --nodb --quiet --eval 'disableTelemetry()'; mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
|
||||||
);
|
);
|
||||||
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");
|
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");
|
||||||
|
|
||||||
@@ -114,17 +124,55 @@ describe("MongoShellHandler", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const command = mongoShellHandler.getConnectionCommand();
|
const command = mongoShellHandler.getConnectionCommand();
|
||||||
|
|
||||||
expect(command).toBe("echo 'Database name not found.'");
|
expect(command).toBe("echo 'Database name not found.'");
|
||||||
|
|
||||||
// Restore original
|
// Restore original
|
||||||
(userContext as UserContextType).databaseAccount = originalDatabaseAccount;
|
(userContext as UserContextType).databaseAccount = originalDatabaseAccount;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return echo if endpoint is missing", () => {
|
||||||
|
const testKey = "test-key";
|
||||||
|
(userContext as UserContextType).databaseAccount = {
|
||||||
|
id: "test-id",
|
||||||
|
name: "", // Empty name to simulate missing name
|
||||||
|
location: "test-location",
|
||||||
|
type: "test-type",
|
||||||
|
kind: "test-kind",
|
||||||
|
properties: { mongoEndpoint: "" },
|
||||||
|
};
|
||||||
|
const mongoShellHandler = new MongoShellHandler(testKey);
|
||||||
|
const command = mongoShellHandler.getConnectionCommand();
|
||||||
|
expect(command).toBe("echo 'MongoDB endpoint not found.'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use _getAadConnectionCommand when _isEntraIdEnabled is true", () => {
|
||||||
|
const testKey = "aad-key";
|
||||||
|
(userContext as UserContextType).databaseAccount = {
|
||||||
|
id: "test-id",
|
||||||
|
name: "test-account",
|
||||||
|
location: "test-location",
|
||||||
|
type: "test-type",
|
||||||
|
kind: "test-kind",
|
||||||
|
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
|
||||||
|
};
|
||||||
|
(userContext as UserContextType).dataPlaneRbacEnabled = true;
|
||||||
|
|
||||||
|
const mongoShellHandler = new MongoShellHandler(testKey);
|
||||||
|
|
||||||
|
const command = mongoShellHandler.getConnectionCommand();
|
||||||
|
expect(command).toContain(
|
||||||
|
"mongosh 'mongodb://test-account:aad-key@test-account.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&authMechanism=PLAIN&retryWrites=false' --tls --tlsAllowInvalidCertificates",
|
||||||
|
);
|
||||||
|
expect(command.startsWith("mongosh --nodb")).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getTerminalSuppressedData", () => {
|
describe("getTerminalSuppressedData", () => {
|
||||||
it("should return the correct warning message", () => {
|
it("should return the correct warning message", () => {
|
||||||
expect(mongoShellHandler.getTerminalSuppressedData()).toBe("Warning: Non-Genuine MongoDB Detected");
|
expect(mongoShellHandler.getTerminalSuppressedData()).toEqual([
|
||||||
|
"Warning: Non-Genuine MongoDB Detected",
|
||||||
|
"Telemetry is now disabled.",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { filterAndCleanTerminalOutput, getHostFromUrl, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||||
|
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
|
||||||
|
|
||||||
export class MongoShellHandler extends AbstractShellHandler {
|
export class MongoShellHandler extends AbstractShellHandler {
|
||||||
private _key: string;
|
private _key: string;
|
||||||
private _endpoint: string | undefined;
|
private _endpoint: string | undefined;
|
||||||
|
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
|
||||||
|
private _isEntraIdEnabled: boolean = isDataplaneRbacEnabledForProxyApi(userContext);
|
||||||
constructor(private key: string) {
|
constructor(private key: string) {
|
||||||
super();
|
super();
|
||||||
this._key = key;
|
this._key = key;
|
||||||
this._endpoint = userContext?.databaseAccount?.properties?.mongoEndpoint;
|
this._endpoint = userContext?.databaseAccount?.properties?.mongoEndpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getKeyConnectionCommand(dbName: string): string {
|
||||||
|
return `mongosh mongodb://${getHostFromUrl(this._endpoint)}:10255?appName=${
|
||||||
|
this.APP_NAME
|
||||||
|
} --username ${dbName} --password ${this._key} --tls --tlsAllowInvalidCertificates`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getAadConnectionCommand(dbName: string): string {
|
||||||
|
return `mongosh 'mongodb://${dbName}:${this._key}@${dbName}.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&authMechanism=PLAIN&retryWrites=false' --tls --tlsAllowInvalidCertificates`;
|
||||||
|
}
|
||||||
|
|
||||||
public getShellName(): string {
|
public getShellName(): string {
|
||||||
return "MongoDB";
|
return "MongoDB";
|
||||||
}
|
}
|
||||||
@@ -28,20 +41,22 @@ export class MongoShellHandler extends AbstractShellHandler {
|
|||||||
if (!dbName) {
|
if (!dbName) {
|
||||||
return "echo 'Database name not found.'";
|
return "echo 'Database name not found.'";
|
||||||
}
|
}
|
||||||
return (
|
const connectionCommand = this._isEntraIdEnabled
|
||||||
"mongosh mongodb://" +
|
? this._getAadConnectionCommand(dbName)
|
||||||
getHostFromUrl(this._endpoint) +
|
: this._getKeyConnectionCommand(dbName);
|
||||||
":10255?appName=" +
|
const fullCommand = `${DISABLE_TELEMETRY_COMMAND}; ${connectionCommand}`;
|
||||||
this.APP_NAME +
|
return fullCommand;
|
||||||
" --username " +
|
|
||||||
dbName +
|
|
||||||
" --password " +
|
|
||||||
this._key +
|
|
||||||
" --tls --tlsAllowInvalidCertificates"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "Warning: Non-Genuine MongoDB Detected";
|
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getExitCommand(): string {
|
||||||
|
return EXIT_COMMAND_MONGO;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTerminalData(data: string): string {
|
||||||
|
return filterAndCleanTerminalOutput(data, this._removeInfoText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ describe("PostgresShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return empty string for terminal suppressed data", () => {
|
it("should return empty string for terminal suppressed data", () => {
|
||||||
expect(postgresShellHandler.getTerminalSuppressedData()).toBe("");
|
expect(postgresShellHandler.getTerminalSuppressedData()).toEqual([""]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class PostgresShellHandler extends AbstractShellHandler {
|
|||||||
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require --set=application_name=${this.APP_NAME}`;
|
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require --set=application_name=${this.APP_NAME}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "";
|
return [""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,24 @@ import { PostgresShellHandler } from "./PostgresShellHandler";
|
|||||||
import { getHandler, getKey } from "./ShellTypeFactory";
|
import { getHandler, getKey } from "./ShellTypeFactory";
|
||||||
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
||||||
|
|
||||||
|
interface UserContextType {
|
||||||
|
databaseAccount: { name: string };
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
features: { enableAadDataPlane: boolean };
|
||||||
|
dataPlaneRbacEnabled: boolean;
|
||||||
|
aadToken?: string;
|
||||||
|
apiType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
jest.mock("../../../../UserContext", () => ({
|
jest.mock("../../../../UserContext", () => ({
|
||||||
userContext: {
|
userContext: {
|
||||||
databaseAccount: { name: "testDbName" },
|
databaseAccount: { name: "testDbName" },
|
||||||
subscriptionId: "testSubId",
|
subscriptionId: "testSubId",
|
||||||
resourceGroup: "testResourceGroup",
|
resourceGroup: "testResourceGroup",
|
||||||
|
features: { enableAadDataPlane: false },
|
||||||
|
dataPlaneRbacEnabled: false,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -109,5 +121,33 @@ describe("ShellTypeHandlerFactory", () => {
|
|||||||
expect(key).toBe(mockKey);
|
expect(key).toBe(mockKey);
|
||||||
expect(listKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName");
|
expect(listKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return MongoShellHandler with primaryMasterKey for TerminalKind.Mongo when RBAC is disabled", async () => {
|
||||||
|
(listKeys as jest.Mock).mockResolvedValue({ primaryMasterKey: "primaryKey123" });
|
||||||
|
(userContext as UserContextType).features.enableAadDataPlane = false;
|
||||||
|
(userContext as UserContextType).dataPlaneRbacEnabled = false;
|
||||||
|
const handler = await getHandler(TerminalKind.Mongo);
|
||||||
|
expect(handler).toBeInstanceOf(MongoShellHandler);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
expect(handler.key).toBe("primaryKey123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return MongoShellHandler with aadToken for TerminalKind.Mongo when RBAC is enabled", async () => {
|
||||||
|
(userContext as UserContextType).aadToken = "aadToken123";
|
||||||
|
(userContext as UserContextType).features.enableAadDataPlane = true;
|
||||||
|
(userContext as UserContextType).dataPlaneRbacEnabled = true;
|
||||||
|
(userContext as UserContextType).apiType = "Mongo";
|
||||||
|
const handler = await getHandler(TerminalKind.Mongo);
|
||||||
|
expect(handler).toBeInstanceOf(MongoShellHandler);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
expect(handler.key).toBe("aadToken123");
|
||||||
|
});
|
||||||
|
it("should throw error for unsupported shell type", async () => {
|
||||||
|
await expect(getHandler("UnknownShell" as unknown as TerminalKind)).rejects.toThrow(
|
||||||
|
"Unsupported shell type: UnknownShell",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
|
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||||
import { CassandraShellHandler } from "./CassandraShellHandler";
|
import { CassandraShellHandler } from "./CassandraShellHandler";
|
||||||
import { MongoShellHandler } from "./MongoShellHandler";
|
import { MongoShellHandler } from "./MongoShellHandler";
|
||||||
@@ -30,6 +31,9 @@ export async function getKey(): Promise<string> {
|
|||||||
if (!dbName) {
|
if (!dbName) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||||
|
return userContext.aadToken || "";
|
||||||
|
}
|
||||||
|
|
||||||
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
||||||
return keys?.primaryMasterKey || "";
|
return keys?.primaryMasterKey || "";
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe("VCoreMongoShellHandler", () => {
|
|||||||
|
|
||||||
expect(Array.isArray(commands)).toBe(true);
|
expect(Array.isArray(commands)).toBe(true);
|
||||||
expect(commands.length).toBe(7);
|
expect(commands.length).toBe(7);
|
||||||
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz");
|
||||||
expect(commands[0]).toContain("mongosh not found");
|
expect(commands[0]).toContain("mongosh not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,7 +57,10 @@ describe("VCoreMongoShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return the correct terminal suppressed data", () => {
|
it("should return the correct terminal suppressed data", () => {
|
||||||
expect(vcoreMongoShellHandler.getTerminalSuppressedData()).toBe("Warning: Non-Genuine MongoDB Detected");
|
expect(vcoreMongoShellHandler.getTerminalSuppressedData()).toEqual([
|
||||||
|
"Warning: Non-Genuine MongoDB Detected",
|
||||||
|
"Telemetry is now disabled.",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||||
|
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
|
||||||
|
|
||||||
export class VCoreMongoShellHandler extends AbstractShellHandler {
|
export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||||
private _endpoint: string | undefined;
|
private _endpoint: string | undefined;
|
||||||
|
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -23,10 +25,24 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
|
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
|
||||||
return `mongosh "mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000&appName=${this.APP_NAME}"`;
|
|
||||||
|
const connectionUri = `mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000&appName=${this.APP_NAME}`;
|
||||||
|
|
||||||
|
return `${DISABLE_TELEMETRY_COMMAND} && mongosh "${connectionUri}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "Warning: Non-Genuine MongoDB Detected";
|
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override getExitCommand to include MongoDB networking guidance
|
||||||
|
*/
|
||||||
|
protected getExitCommand(): string {
|
||||||
|
return EXIT_COMMAND_MONGO;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTerminalData(data: string): string {
|
||||||
|
return filterAndCleanTerminalOutput(data, this._removeInfoText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,18 @@ export class AttachAddon implements ITerminalAddon {
|
|||||||
* @param {Terminal} terminal - The XTerm terminal instance
|
* @param {Terminal} terminal - The XTerm terminal instance
|
||||||
*/
|
*/
|
||||||
public addMessageListener(terminal: Terminal): void {
|
public addMessageListener(terminal: Terminal): void {
|
||||||
|
let messageBuffer = "";
|
||||||
|
let bufferTimeout: NodeJS.Timeout | null = null;
|
||||||
|
const BUFFER_TIMEOUT = 50; // ms - short timeout for prompt detection
|
||||||
|
|
||||||
|
const processBuffer = () => {
|
||||||
|
if (messageBuffer.length > 0) {
|
||||||
|
this.handleCompleteTerminalData(terminal, messageBuffer);
|
||||||
|
messageBuffer = "";
|
||||||
|
}
|
||||||
|
bufferTimeout = null;
|
||||||
|
};
|
||||||
|
|
||||||
this._disposables.push(
|
this._disposables.push(
|
||||||
addSocketListener(this._socket, "message", (ev) => {
|
addSocketListener(this._socket, "message", (ev) => {
|
||||||
let data: ArrayBuffer | string = ev.data;
|
let data: ArrayBuffer | string = ev.data;
|
||||||
@@ -103,51 +115,136 @@ export class AttachAddon implements ITerminalAddon {
|
|||||||
data = enc.decode(ev.data as ArrayBuffer);
|
data = enc.decode(ev.data as ArrayBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for example of json object look in TerminalHelper in the socket.onMessage
|
// Handle status messages
|
||||||
if (data.includes(startStatusJson) && data.includes(endStatusJson)) {
|
let processedStatusData = data;
|
||||||
// process as one line
|
|
||||||
const statusData = data.split(startStatusJson)[1].split(endStatusJson)[0];
|
|
||||||
data = data.replace(statusData, "");
|
|
||||||
data = data.replace(startStatusJson, "");
|
|
||||||
data = data.replace(endStatusJson, "");
|
|
||||||
} else if (data.includes(startStatusJson)) {
|
|
||||||
// check for start
|
|
||||||
const partialStatusData = data.split(startStatusJson)[1];
|
|
||||||
this._socketData += partialStatusData;
|
|
||||||
data = data.replace(partialStatusData, "");
|
|
||||||
data = data.replace(startStatusJson, "");
|
|
||||||
} else if (data.includes(endStatusJson)) {
|
|
||||||
// check for end and process the command
|
|
||||||
const partialStatusData = data.split(endStatusJson)[0];
|
|
||||||
this._socketData += partialStatusData;
|
|
||||||
data = data.replace(partialStatusData, "");
|
|
||||||
data = data.replace(endStatusJson, "");
|
|
||||||
this._socketData = "";
|
|
||||||
} else if (this._socketData.length > 0) {
|
|
||||||
// check if the line is all data then just concatenate
|
|
||||||
this._socketData += data;
|
|
||||||
data = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._allowTerminalWrite && data.includes(this._startMarker)) {
|
// Process status messages with delimiters
|
||||||
this._allowTerminalWrite = false;
|
// eslint-disable-next-line no-constant-condition
|
||||||
terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`);
|
while (true) {
|
||||||
}
|
const startIndex = processedStatusData.indexOf(startStatusJson);
|
||||||
|
if (startIndex === -1) {
|
||||||
if (this._allowTerminalWrite) {
|
break;
|
||||||
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
|
|
||||||
const hasSuppressedData = suppressedData && suppressedData.length > 0;
|
|
||||||
|
|
||||||
if (!hasSuppressedData || !data.includes(suppressedData)) {
|
|
||||||
terminal.write(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const afterStart = processedStatusData.substring(startIndex + startStatusJson.length);
|
||||||
|
const endIndex = afterStart.indexOf(endStatusJson);
|
||||||
|
|
||||||
|
if (endIndex === -1) {
|
||||||
|
// Incomplete status message
|
||||||
|
this._socketData += processedStatusData.substring(startIndex);
|
||||||
|
processedStatusData = processedStatusData.substring(0, startIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove processed status message
|
||||||
|
processedStatusData =
|
||||||
|
processedStatusData.substring(0, startIndex) + afterStart.substring(endIndex + endStatusJson.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.includes(this._shellHandler.getConnectionCommand())) {
|
// Add to message buffer
|
||||||
this._allowTerminalWrite = true;
|
messageBuffer += processedStatusData;
|
||||||
|
|
||||||
|
// Clear existing timeout
|
||||||
|
if (bufferTimeout) {
|
||||||
|
clearTimeout(bufferTimeout);
|
||||||
|
bufferTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this looks like a complete message/command
|
||||||
|
const isComplete = this.isMessageComplete(messageBuffer, processedStatusData);
|
||||||
|
|
||||||
|
if (isComplete) {
|
||||||
|
// Message marked as complete, processing immediately
|
||||||
|
processBuffer();
|
||||||
|
} else {
|
||||||
|
// Set timeout to process buffer after delay
|
||||||
|
bufferTimeout = setTimeout(processBuffer, BUFFER_TIMEOUT);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Clean up timeout on dispose
|
||||||
|
this._disposables.push({
|
||||||
|
dispose: () => {
|
||||||
|
if (bufferTimeout) {
|
||||||
|
clearTimeout(bufferTimeout);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private isMessageComplete(fullBuffer: string, currentChunk: string): boolean {
|
||||||
|
// Immediate completion indicators
|
||||||
|
const immediateCompletionPatterns = [
|
||||||
|
/\n$/, // Ends with newline
|
||||||
|
/\r$/, // Ends with carriage return
|
||||||
|
/\r\n$/, // Ends with CRLF
|
||||||
|
/; \} \|\| true;$/, // Your command pattern
|
||||||
|
/disown -a && exit$/, // Exit commands
|
||||||
|
/printf.*?\\033\[0m\\n"$/, // Your printf pattern
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check current chunk for immediate completion
|
||||||
|
for (const pattern of immediateCompletionPatterns) {
|
||||||
|
if (pattern.test(currentChunk)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANSI sequence detection - these might be complete prompts
|
||||||
|
const ansiPromptPatterns = [
|
||||||
|
/\[\d+G\[0J.*>\s*\[\d+G$/, // Your specific pattern: [1G[0J...> [26G
|
||||||
|
/\[\d+;\d+H/, // Cursor position sequences
|
||||||
|
/\]\s*\[\d+G$/, // Ends with cursor positioning
|
||||||
|
/>\s*\[\d+G$/, // Prompt followed by cursor position
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if buffer ends with what looks like a complete prompt
|
||||||
|
for (const pattern of ansiPromptPatterns) {
|
||||||
|
if (pattern.test(fullBuffer)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for MongoDB shell prompts specifically
|
||||||
|
const mongoPromptPatterns = [
|
||||||
|
/globaldb \[primary\] \w+>\s*\[\d+G$/, // MongoDB replica set prompt
|
||||||
|
/>\s*\[\d+G$/, // General prompt with cursor positioning
|
||||||
|
/\w+>\s*$/, // Simple shell prompt
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of mongoPromptPatterns) {
|
||||||
|
if (pattern.test(fullBuffer)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCompleteTerminalData(terminal: Terminal, data: string): void {
|
||||||
|
if (this._allowTerminalWrite && data.includes(this._startMarker)) {
|
||||||
|
this._allowTerminalWrite = false;
|
||||||
|
terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._allowTerminalWrite) {
|
||||||
|
const updatedData =
|
||||||
|
typeof this._shellHandler?.updateTerminalData === "function"
|
||||||
|
? this._shellHandler.updateTerminalData(data)
|
||||||
|
: data;
|
||||||
|
|
||||||
|
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
|
||||||
|
const shouldNotWrite = suppressedData.filter(Boolean).some((item) => updatedData.includes(item));
|
||||||
|
|
||||||
|
if (!shouldNotWrite) {
|
||||||
|
terminal.write(updatedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.includes(this._shellHandler.getConnectionCommand())) {
|
||||||
|
this._allowTerminalWrite = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
|
|||||||
@@ -50,3 +50,34 @@ export const getShellNameForDisplay = (terminalKind: TerminalKind): string => {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get MongoDB shell information text that should be removed from terminal output
|
||||||
|
*/
|
||||||
|
export const getMongoShellRemoveInfoText = (): string[] => {
|
||||||
|
return [
|
||||||
|
"For mongosh info see: https://www.mongodb.com/docs/mongodb-shell/",
|
||||||
|
"disableTelemetry() command",
|
||||||
|
"https://www.mongodb.com/legal/privacy-policy",
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterAndCleanTerminalOutput = (data: string, removeInfoText: string[]): string => {
|
||||||
|
if (!data || removeInfoText.length === 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = data.split("\n");
|
||||||
|
const filteredLines: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const shouldRemove = removeInfoText.some((text) => line.includes(text));
|
||||||
|
|
||||||
|
if (!shouldRemove) {
|
||||||
|
filteredLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredLines.join("\n").replace(/((\r\n)|\n|\r){2,}/g, "\r\n");
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const validCloudShellRegions = new Set([
|
|||||||
"westeurope",
|
"westeurope",
|
||||||
"centralindia",
|
"centralindia",
|
||||||
"southeastasia",
|
"southeastasia",
|
||||||
"westcentralus",
|
"usgovvirginia",
|
||||||
|
"usgovarizona",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,7 +40,6 @@ export const getNormalizedRegion = (region: string, defaultCloudshellRegion: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
const regionMap: Record<string, string> = {
|
const regionMap: Record<string, string> = {
|
||||||
centralus: "westcentralus",
|
|
||||||
eastus2: "eastus",
|
eastus2: "eastus",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -146,10 +146,16 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
updateConfigContext({ platform: Platform.Hosted });
|
updateConfigContext({ platform: Platform.Hosted });
|
||||||
|
|
||||||
const props: IDocumentsTabComponentProps = createMockProps();
|
const props: IDocumentsTabComponentProps = createMockProps();
|
||||||
|
|
||||||
wrapper = mount(<DocumentsTabComponent {...props} />);
|
wrapper = mount(<DocumentsTabComponent {...props} />);
|
||||||
wrapper = await waitForComponentToPaint(wrapper);
|
|
||||||
});
|
// Wait for all pending promises
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for any async operations to complete
|
||||||
|
wrapper = await waitForComponentToPaint(wrapper, 100);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export interface IGraphConfig {
|
|||||||
|
|
||||||
interface GraphTabOptions extends ViewModels.TabOptions {
|
interface GraphTabOptions extends ViewModels.TabOptions {
|
||||||
account: DatabaseAccount;
|
account: DatabaseAccount;
|
||||||
masterKey: string;
|
password: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
collectionPartitionKeyProperty: string;
|
collectionPartitionKeyProperty: string;
|
||||||
@@ -107,7 +107,7 @@ export default class GraphTab extends TabsBase {
|
|||||||
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
|
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
|
||||||
databaseId: options.databaseId,
|
databaseId: options.databaseId,
|
||||||
collectionId: options.collectionId,
|
collectionId: options.collectionId,
|
||||||
masterKey: options.masterKey,
|
password: options.password,
|
||||||
onLoadStartKey: options.onLoadStartKey,
|
onLoadStartKey: options.onLoadStartKey,
|
||||||
onLoadStartKeyChange: (onLoadStartKey: number): void => {
|
onLoadStartKeyChange: (onLoadStartKey: number): void => {
|
||||||
if (onLoadStartKey === undefined) {
|
if (onLoadStartKey === undefined) {
|
||||||
|
|||||||
@@ -106,6 +106,6 @@ describe("QueryTabComponent", () => {
|
|||||||
<QueryTabCopilotComponent {...propsMock} />
|
<QueryTabCopilotComponent {...propsMock} />
|
||||||
</CopilotProvider>,
|
</CopilotProvider>,
|
||||||
);
|
);
|
||||||
expect(container.find(QueryCopilotPromptbar).exists()).toBe(true);
|
expect(container.find(QueryCopilotPromptbar).exists()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useDialog } from "Explorer/Controls/Dialog";
|
|||||||
import { monaco } from "Explorer/LazyMonaco";
|
import { monaco } from "Explorer/LazyMonaco";
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
|
||||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
@@ -28,8 +27,9 @@ import { TabsState, useTabs } from "hooks/useTabs";
|
|||||||
import React, { Fragment, createRef } from "react";
|
import React, { Fragment, createRef } from "react";
|
||||||
import "react-splitter-layout/lib/index.css";
|
import "react-splitter-layout/lib/index.css";
|
||||||
import { format } from "react-string-format";
|
import { format } from "react-string-format";
|
||||||
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
//TODO: Uncomment next two lines when query copilot is reinstated in DE
|
||||||
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
||||||
|
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||||
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
|
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
|
||||||
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
@@ -494,53 +494,55 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
|
//TODO: Uncomment next section when query copilot is reinstated in DE
|
||||||
const mainButtonLabel = "Launch Copilot";
|
// if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
|
||||||
const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
|
// const mainButtonLabel = "Launch Copilot";
|
||||||
const copilotSettingLabel = "Copilot settings";
|
// const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
|
||||||
|
// const copilotSettingLabel = "Copilot settings";
|
||||||
|
|
||||||
const openCopilotChatButton: CommandButtonComponentProps = {
|
// const openCopilotChatButton: CommandButtonComponentProps = {
|
||||||
iconAlt: chatPaneLabel,
|
// iconAlt: chatPaneLabel,
|
||||||
onCommandClick: this.launchQueryCopilotChat,
|
// onCommandClick: this.launchQueryCopilotChat,
|
||||||
commandButtonLabel: chatPaneLabel,
|
// commandButtonLabel: chatPaneLabel,
|
||||||
ariaLabel: chatPaneLabel,
|
// ariaLabel: chatPaneLabel,
|
||||||
hasPopup: false,
|
// hasPopup: false,
|
||||||
};
|
// };
|
||||||
|
|
||||||
const copilotSettingsButton: CommandButtonComponentProps = {
|
// const copilotSettingsButton: CommandButtonComponentProps = {
|
||||||
iconAlt: copilotSettingLabel,
|
// iconAlt: copilotSettingLabel,
|
||||||
onCommandClick: () => undefined,
|
// onCommandClick: () => undefined,
|
||||||
commandButtonLabel: copilotSettingLabel,
|
// commandButtonLabel: copilotSettingLabel,
|
||||||
ariaLabel: copilotSettingLabel,
|
// ariaLabel: copilotSettingLabel,
|
||||||
hasPopup: false,
|
// hasPopup: false,
|
||||||
};
|
// };
|
||||||
|
|
||||||
const launchCopilotButton: CommandButtonComponentProps = {
|
// const launchCopilotButton: CommandButtonComponentProps = {
|
||||||
iconSrc: LaunchCopilot,
|
// iconSrc: LaunchCopilot,
|
||||||
iconAlt: mainButtonLabel,
|
// iconAlt: mainButtonLabel,
|
||||||
onCommandClick: this.launchQueryCopilotChat,
|
// onCommandClick: this.launchQueryCopilotChat,
|
||||||
commandButtonLabel: mainButtonLabel,
|
// commandButtonLabel: mainButtonLabel,
|
||||||
ariaLabel: mainButtonLabel,
|
// ariaLabel: mainButtonLabel,
|
||||||
hasPopup: false,
|
// hasPopup: false,
|
||||||
children: [openCopilotChatButton, copilotSettingsButton],
|
// children: [openCopilotChatButton, copilotSettingsButton],
|
||||||
};
|
// };
|
||||||
buttons.push(launchCopilotButton);
|
// buttons.push(launchCopilotButton);
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (this.props.copilotEnabled) {
|
//TODO: Uncomment next section when query copilot is reinstated in DE
|
||||||
const toggleCopilotButton: CommandButtonComponentProps = {
|
// if (this.props.copilotEnabled) {
|
||||||
iconSrc: QueryCommandIcon,
|
// const toggleCopilotButton: CommandButtonComponentProps = {
|
||||||
iconAlt: "Query Advisor",
|
// iconSrc: QueryCommandIcon,
|
||||||
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
|
// iconAlt: "Query Advisor",
|
||||||
onCommandClick: () => {
|
// keyboardAction: KeyboardAction.TOGGLE_COPILOT,
|
||||||
this._toggleCopilot(!this.state.copilotActive);
|
// onCommandClick: () => {
|
||||||
},
|
// this._toggleCopilot(!this.state.copilotActive);
|
||||||
commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
|
// },
|
||||||
ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
|
// commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
|
||||||
hasPopup: false,
|
// ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
|
||||||
};
|
// hasPopup: false,
|
||||||
buttons.push(toggleCopilotButton);
|
// };
|
||||||
}
|
// buttons.push(toggleCopilotButton);
|
||||||
|
// }
|
||||||
|
|
||||||
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
|
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
|
||||||
const label = "Cancel query";
|
const label = "Cancel query";
|
||||||
@@ -725,6 +727,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<CosmosFluentProvider id={this.props.tabId} className={this.props.styles.queryTab} role="tabpanel">
|
<CosmosFluentProvider id={this.props.tabId} className={this.props.styles.queryTab} role="tabpanel">
|
||||||
|
{/*TODO: Uncomment this section when query copilot is reinstated in DE
|
||||||
{this.props.copilotEnabled && this.state.currentTabActive && this.state.copilotActive && (
|
{this.props.copilotEnabled && this.state.currentTabActive && this.state.copilotActive && (
|
||||||
<QueryCopilotPromptbar
|
<QueryCopilotPromptbar
|
||||||
explorer={this.props.collection.container}
|
explorer={this.props.collection.container}
|
||||||
@@ -732,7 +735,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
databaseId={this.props.collection.databaseId}
|
databaseId={this.props.collection.databaseId}
|
||||||
containerId={this.props.collection.id()}
|
containerId={this.props.collection.id()}
|
||||||
></QueryCopilotPromptbar>
|
></QueryCopilotPromptbar>
|
||||||
)}
|
)} */}
|
||||||
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
||||||
<Allotment
|
<Allotment
|
||||||
key={vertical.toString()}
|
key={vertical.toString()}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFi
|
|||||||
import { getShellNameForDisplay } from "Explorer/Tabs/CloudShellTab/Utils/CommonUtils";
|
import { getShellNameForDisplay } from "Explorer/Tabs/CloudShellTab/Utils/CommonUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import FirewallRuleScreenshot from "../../../../images/firewallRule.png";
|
import FirewallRuleScreenshot from "../../../../images/firewallRule.png";
|
||||||
import VcoreFirewallRuleScreenshot from "../../../../images/vcoreMongoFirewallRule.png";
|
|
||||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
@@ -25,15 +24,15 @@ export abstract class BaseTerminalComponentAdapter implements ReactAdapter {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
|
if (this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo) {
|
||||||
|
return this.renderTerminalComponent();
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isAllPublicIPAddressesEnabled()) {
|
if (!this.isAllPublicIPAddressesEnabled()) {
|
||||||
return (
|
return (
|
||||||
<QuickstartFirewallNotification
|
<QuickstartFirewallNotification
|
||||||
messageType={this.getMessageType()}
|
messageType={this.getMessageType()}
|
||||||
screenshot={
|
screenshot={FirewallRuleScreenshot}
|
||||||
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
|
|
||||||
? VcoreFirewallRuleScreenshot
|
|
||||||
: FirewallRuleScreenshot
|
|
||||||
}
|
|
||||||
shellName={getShellNameForDisplay(this.kind)}
|
shellName={getShellNameForDisplay(this.kind)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||||
|
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -479,9 +480,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
node: this,
|
node: this,
|
||||||
title: title,
|
title: title,
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
|
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||||
collection: this,
|
collection: this,
|
||||||
masterKey: userContext.masterKey || "",
|
|
||||||
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
||||||
collectionId: this.id(),
|
collectionId: this.id(),
|
||||||
databaseId: this.databaseId,
|
databaseId: this.databaseId,
|
||||||
@@ -737,7 +737,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
title: title,
|
title: title,
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
collection: this,
|
collection: this,
|
||||||
masterKey: userContext.masterKey || "",
|
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||||
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
||||||
collectionId: this.id(),
|
collectionId: this.id(),
|
||||||
databaseId: this.databaseId,
|
databaseId: this.databaseId,
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -23,6 +28,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -45,6 +55,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -65,6 +80,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -123,6 +143,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -138,6 +163,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -187,6 +217,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -257,6 +292,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -323,7 +363,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -354,7 +394,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -386,7 +426,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -422,7 +462,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -490,7 +530,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -521,7 +561,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -580,7 +620,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -666,7 +706,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ const App: React.FunctionComponent = () => {
|
|||||||
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
|
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
|
||||||
useAADAuth();
|
useAADAuth(config);
|
||||||
|
|
||||||
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
||||||
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
||||||
const [connectionString, setConnectionString] = React.useState<string>();
|
const [connectionString, setConnectionString] = React.useState<string>();
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export class PhoenixClient {
|
|||||||
|
|
||||||
private getPhoenixControlPlanePathPrefix(): string {
|
private getPhoenixControlPlanePathPrefix(): string {
|
||||||
if (!this.armResourceId) {
|
if (!this.armResourceId) {
|
||||||
throw new Error("The Phoenix client was not initialized properly: missing ARM resourcce id");
|
throw new Error("The Phoenix client was not initialized properly: missing ARM resource id");
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolsEndpoint =
|
const toolsEndpoint =
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
|||||||
import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
|
import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
|
||||||
import { FabricArtifactInfo, updateUserContext, userContext } from "UserContext";
|
import { FabricArtifactInfo, updateUserContext, userContext } from "UserContext";
|
||||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
|
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
|
||||||
|
|
||||||
|
// Fabric Native accounts are always autoscale and have a fixed throughput of 5K
|
||||||
|
export const DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT = AutoPilotUtils.autoPilotThroughput5K;
|
||||||
|
|
||||||
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
|
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
|
||||||
const DEBOUNCE_DELAY_MS = 1000 * 20; // 20 second
|
const DEBOUNCE_DELAY_MS = 1000 * 20; // 20 second
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
enableCloudShell: "true" === get("enablecloudshell"),
|
enableCloudShell: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
|||||||
host: configContext.CATALOG_ENDPOINT,
|
host: configContext.CATALOG_ENDPOINT,
|
||||||
path: getOfferingIdPathForRegion(),
|
path: getOfferingIdPathForRegion(),
|
||||||
method: "GET",
|
method: "GET",
|
||||||
apiVersion: "2023-05-01-preview",
|
apiVersion: configContext.CATALOG_API_VERSION,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
filter:
|
filter:
|
||||||
"armRegionName eq '" +
|
"armRegionName eq '" +
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MongoGuidRepresentation } from "Common/Constants";
|
||||||
import { SplitterDirection } from "Common/Splitter";
|
import { SplitterDirection } from "Common/Splitter";
|
||||||
import * as LocalStorageUtility from "./LocalStorageUtility";
|
import * as LocalStorageUtility from "./LocalStorageUtility";
|
||||||
import * as SessionStorageUtility from "./SessionStorageUtility";
|
import * as SessionStorageUtility from "./SessionStorageUtility";
|
||||||
@@ -33,6 +34,8 @@ export enum StorageKey {
|
|||||||
DocumentsTabPrefs,
|
DocumentsTabPrefs,
|
||||||
DefaultQueryResultsView,
|
DefaultQueryResultsView,
|
||||||
AppState,
|
AppState,
|
||||||
|
MongoGuidRepresentation,
|
||||||
|
IgnorePartitionKeyOnDocumentUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasRUThresholdBeenConfigured = (): boolean => {
|
export const hasRUThresholdBeenConfigured = (): boolean => {
|
||||||
@@ -65,4 +68,13 @@ export const getDefaultQueryResultsView = (): SplitterDirection => {
|
|||||||
return SplitterDirection.Horizontal;
|
return SplitterDirection.Horizontal;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMongoGuidRepresentation = (): MongoGuidRepresentation => {
|
||||||
|
const mongoGuidRepresentation: string | null = LocalStorageUtility.getEntryString(StorageKey.MongoGuidRepresentation);
|
||||||
|
if (mongoGuidRepresentation) {
|
||||||
|
return mongoGuidRepresentation as MongoGuidRepresentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MongoGuidRepresentation.CSharpLegacy;
|
||||||
|
};
|
||||||
|
|
||||||
export const DefaultRUThreshold = 5000;
|
export const DefaultRUThreshold = 5000;
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export enum Action {
|
|||||||
CloudShellUserConsent,
|
CloudShellUserConsent,
|
||||||
CloudShellTerminalSession,
|
CloudShellTerminalSession,
|
||||||
OpenVSCode,
|
OpenVSCode,
|
||||||
|
ImportSampleData,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionModifiers = {
|
export const ActionModifiers = {
|
||||||
|
|||||||
@@ -91,5 +91,11 @@ export const getItemName = (): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isDataplaneRbacSupported = (apiType: string): boolean => {
|
export const isDataplaneRbacSupported = (apiType: string): boolean => {
|
||||||
return apiType === "SQL" || apiType === "Tables";
|
return (
|
||||||
|
apiType === "SQL" || apiType === "Tables" || apiType === "Gremlin" || apiType === "Mongo" || apiType === "Cassandra"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasProxyServer = (apiType: string): boolean => {
|
||||||
|
return apiType === "Mongo" || apiType === "Cassandra";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,51 @@
|
|||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { ApiType, updateUserContext, userContext } from "../UserContext";
|
||||||
import * as AuthorizationUtils from "./AuthorizationUtils";
|
import * as AuthorizationUtils from "./AuthorizationUtils";
|
||||||
jest.mock("../Explorer/Explorer");
|
jest.mock("../Explorer/Explorer");
|
||||||
|
|
||||||
describe("AuthorizationUtils", () => {
|
describe("AuthorizationUtils", () => {
|
||||||
|
const setAadDataPlane = (enabled: boolean) => {
|
||||||
|
updateUserContext({
|
||||||
|
features: {
|
||||||
|
enableAadDataPlane: enabled,
|
||||||
|
canExceedMaximumValue: false,
|
||||||
|
cosmosdb: false,
|
||||||
|
enableChangeFeedPolicy: false,
|
||||||
|
enableFixedCollectionWithSharedThroughput: false,
|
||||||
|
enableKOPanel: false,
|
||||||
|
enableNotebooks: false,
|
||||||
|
enableReactPane: false,
|
||||||
|
enableRightPanelV2: false,
|
||||||
|
enableSchema: false,
|
||||||
|
enableSDKoperations: false,
|
||||||
|
enableSpark: false,
|
||||||
|
enableTtl: false,
|
||||||
|
executeSproc: false,
|
||||||
|
enableResourceGraph: false,
|
||||||
|
enableKoResourceTree: false,
|
||||||
|
enableThroughputBuckets: false,
|
||||||
|
hostedDataExplorer: false,
|
||||||
|
sandboxNotebookOutputs: false,
|
||||||
|
showMinRUSurvey: false,
|
||||||
|
ttl90Days: false,
|
||||||
|
enableThroughputCap: false,
|
||||||
|
enableHierarchicalKeys: false,
|
||||||
|
enableCopilot: false,
|
||||||
|
disableCopilotPhoenixGateaway: false,
|
||||||
|
enableCopilotFullSchema: false,
|
||||||
|
copilotChatFixedMonacoEditorHeight: false,
|
||||||
|
enablePriorityBasedExecution: false,
|
||||||
|
disableConnectionStringLogin: false,
|
||||||
|
enableCloudShell: false,
|
||||||
|
autoscaleDefault: false,
|
||||||
|
partitionKeyDefault: false,
|
||||||
|
partitionKeyDefault2: false,
|
||||||
|
notebooksDownBanner: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
describe("getAuthorizationHeader()", () => {
|
describe("getAuthorizationHeader()", () => {
|
||||||
it("should return authorization header if authentication type is AAD", () => {
|
it("should return authorization header if authentication type is AAD", () => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
@@ -54,4 +95,41 @@ describe("AuthorizationUtils", () => {
|
|||||||
).toBeDefined();
|
).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("useDataplaneRbacAuthorization()", () => {
|
||||||
|
it("should return true if enableAadDataPlane feature flag is set", () => {
|
||||||
|
setAadDataPlane(true);
|
||||||
|
expect(AuthorizationUtils.useDataplaneRbacAuthorization(userContext)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true if dataPlaneRbacEnabled is set to true and API supports RBAC", () => {
|
||||||
|
setAadDataPlane(false);
|
||||||
|
["SQL", "Tables", "Gremlin", "Mongo", "Cassandra"].forEach((type) => {
|
||||||
|
updateUserContext({
|
||||||
|
dataPlaneRbacEnabled: true,
|
||||||
|
apiType: type as ApiType,
|
||||||
|
});
|
||||||
|
expect(AuthorizationUtils.useDataplaneRbacAuthorization(userContext)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if dataPlaneRbacEnabled is set to true and API does not support RBAC", () => {
|
||||||
|
setAadDataPlane(false);
|
||||||
|
["Postgres", "VCoreMongo"].forEach((type) => {
|
||||||
|
updateUserContext({
|
||||||
|
dataPlaneRbacEnabled: true,
|
||||||
|
apiType: type as ApiType,
|
||||||
|
});
|
||||||
|
expect(AuthorizationUtils.useDataplaneRbacAuthorization(userContext)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if dataPlaneRbacEnabled is set to false", () => {
|
||||||
|
setAadDataPlane(false);
|
||||||
|
updateUserContext({
|
||||||
|
dataPlaneRbacEnabled: false,
|
||||||
|
});
|
||||||
|
expect(AuthorizationUtils.useDataplaneRbacAuthorization(userContext)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import * as msal from "@azure/msal-browser";
|
import * as msal from "@azure/msal-browser";
|
||||||
|
import { getEnvironmentScopeEndpoint } from "Common/EnvironmentUtility";
|
||||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { hasProxyServer, isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
@@ -7,7 +9,7 @@ import { configContext } from "../ConfigContext";
|
|||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../UserContext";
|
import { UserContext, userContext } from "../UserContext";
|
||||||
|
|
||||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||||
if (userContext.authType === AuthType.EncryptedToken) {
|
if (userContext.authType === AuthType.EncryptedToken) {
|
||||||
@@ -73,10 +75,12 @@ export async function acquireMsalTokenForAccount(
|
|||||||
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
|
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
|
||||||
throw new Error("Database account has no document endpoint defined");
|
throw new Error("Database account has no document endpoint defined");
|
||||||
}
|
}
|
||||||
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
let hrefEndpoint = "";
|
||||||
/\/+$/,
|
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||||
"/.default",
|
hrefEndpoint = getEnvironmentScopeEndpoint();
|
||||||
);
|
} else {
|
||||||
|
hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(/\/+$/, "/.default");
|
||||||
|
}
|
||||||
const msalInstance = await getMsalInstance();
|
const msalInstance = await getMsalInstance();
|
||||||
const knownAccounts = msalInstance.getAllAccounts();
|
const knownAccounts = msalInstance.getAllAccounts();
|
||||||
// If user_hint is provided, we will try to use it to find the account.
|
// If user_hint is provided, we will try to use it to find the account.
|
||||||
@@ -179,3 +183,14 @@ export async function acquireTokenWithMsal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDataplaneRbacAuthorization(userContext: UserContext): boolean {
|
||||||
|
return (
|
||||||
|
userContext.features?.enableAadDataPlane ||
|
||||||
|
(userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDataplaneRbacEnabledForProxyApi(userContext: UserContext): boolean {
|
||||||
|
return useDataplaneRbacAuthorization(userContext) && hasProxyServer(userContext.apiType);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const autoPilotThroughput1K = 1000;
|
export const autoPilotThroughput1K = 1000;
|
||||||
export const autoPilotIncrementStep = 1000;
|
export const autoPilotIncrementStep = 1000;
|
||||||
export const autoPilotThroughput4K = 4000;
|
export const autoPilotThroughput4K = 4000;
|
||||||
|
export const autoPilotThroughput5K = 5000;
|
||||||
export const autoPilotThroughput10K = 10000;
|
export const autoPilotThroughput10K = 10000;
|
||||||
|
|
||||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
@@ -18,5 +19,8 @@ export const isServerlessAccount = (): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isVectorSearchEnabled = (): boolean => {
|
export const isVectorSearchEnabled = (): boolean => {
|
||||||
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch);
|
return (
|
||||||
|
userContext.apiType === "SQL" &&
|
||||||
|
(isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch) || isFabricNative())
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,32 +45,25 @@ export const defaultAllowedArmEndpoints: ReadonlyArray<string> = [
|
|||||||
"https://management.chinacloudapi.cn",
|
"https://management.chinacloudapi.cn",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const allowedAadEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedAadEndpoints: ReadonlyArray<string> = [
|
||||||
"https://login.microsoftonline.com/",
|
"https://login.microsoftonline.com/",
|
||||||
"https://login.microsoftonline.us/",
|
"https://login.microsoftonline.us/",
|
||||||
"https://login.partner.microsoftonline.cn/",
|
"https://login.partner.microsoftonline.cn/",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const defaultAllowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.microsoft.com"];
|
||||||
|
|
||||||
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
||||||
"https://localhost:12901",
|
|
||||||
"https://localhost:1234",
|
"https://localhost:1234",
|
||||||
|
PortalBackendEndpoints.Development,
|
||||||
|
PortalBackendEndpoints.Mpac,
|
||||||
|
PortalBackendEndpoints.Prod,
|
||||||
|
PortalBackendEndpoints.Fairfax,
|
||||||
|
PortalBackendEndpoints.Mooncake,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const PortalBackendOutboundIPs: { [key: string]: string[] } = {
|
|
||||||
[PortalBackendEndpoints.Mpac]: ["13.91.105.215", "4.210.172.107"],
|
|
||||||
[PortalBackendEndpoints.Prod]: ["13.88.56.148", "40.91.218.243"],
|
|
||||||
[PortalBackendEndpoints.Fairfax]: ["52.247.163.6", "52.244.134.181"],
|
|
||||||
[PortalBackendEndpoints.Mooncake]: ["163.228.137.6", "143.64.170.142"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
|
||||||
[MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"],
|
|
||||||
[MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"],
|
|
||||||
[MongoProxyEndpoints.Fairfax]: ["52.244.176.112", "52.247.148.42"],
|
|
||||||
[MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||||
|
"https://localhost:1234",
|
||||||
MongoProxyEndpoints.Development,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
@@ -86,19 +79,8 @@ export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
|||||||
CassandraProxyEndpoints.Mooncake,
|
CassandraProxyEndpoints.Mooncake,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CassandraProxyOutboundIPs: { [key: string]: string[] } = {
|
|
||||||
[CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"],
|
|
||||||
[CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"],
|
|
||||||
[CassandraProxyEndpoints.Fairfax]: ["52.244.50.101", "52.227.165.24"],
|
|
||||||
[CassandraProxyEndpoints.Mooncake]: ["40.73.99.146", "143.64.62.47"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
||||||
|
|
||||||
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
|
|
||||||
|
|
||||||
export const allowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.microsoft.com"];
|
|
||||||
|
|
||||||
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];
|
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];
|
||||||
|
|
||||||
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
|
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user