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/QueriesClient.ts
|
||||
src/Common/Splitter.ts
|
||||
src/Controls/Heatmap/Heatmap.test.ts
|
||||
src/Controls/Heatmap/Heatmap.ts
|
||||
src/Definitions/datatables.d.ts
|
||||
src/Definitions/gif.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"
|
||||
uses: Azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
|
||||
- name: "Acquire RBAC tokens for test accounts"
|
||||
uses: azure/cli@v2
|
||||
with:
|
||||
azcliversion: latest
|
||||
inlineScript: |
|
||||
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
|
||||
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
||||
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
||||
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
|
||||
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
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']}}
|
||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
||||
- 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"
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
|
||||
2
.npmrc
2
.npmrc
@@ -1,4 +1,4 @@
|
||||
save-exact=true
|
||||
|
||||
# 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",
|
||||
"isTerminalEnabled": true,
|
||||
"isPhoenixEnabled": true
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isTerminalEnabled" : false,
|
||||
"isPhoenixEnabled" : false
|
||||
}
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"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,
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.3.0",
|
||||
"@azure/cosmos": "4.5.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "4.5.0",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
@@ -391,24 +391,25 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@azure/cosmos": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.3.0.tgz",
|
||||
"integrity": "sha512-0Ls3l1uWBBSphx6YRhnM+w7rSvq8qVugBCdO6kSiNuRYXEf6+YWLjbzz4e7L2kkz/6ScFdZIOJYP+XtkiRYOhA==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.5.0.tgz",
|
||||
"integrity": "sha512-JsTh4twb6FcwP7rJwxQiNZQ/LGtuF6gmciaxY9Rnp6/A325Lhsw/SH4R2ArpT0yCvozbZpweIwdPfUkXVBtp5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-auth": "^1.7.1",
|
||||
"@azure/core-rest-pipeline": "^1.15.1",
|
||||
"@azure/core-tracing": "^1.1.1",
|
||||
"@azure/core-util": "^1.8.1",
|
||||
"@azure/keyvault-keys": "^4.8.0",
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
"@azure/core-auth": "^1.9.0",
|
||||
"@azure/core-rest-pipeline": "^1.19.1",
|
||||
"@azure/core-tracing": "^1.2.0",
|
||||
"@azure/core-util": "^1.11.0",
|
||||
"@azure/keyvault-keys": "^4.9.0",
|
||||
"@azure/logger": "^1.1.4",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"jsbi": "^4.3.0",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"semaphore": "^1.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/cosmos-language-service": {
|
||||
@@ -438,8 +439,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/cosmos/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"license": "0BSD"
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/@azure/identity": {
|
||||
"version": "4.5.0",
|
||||
@@ -27178,11 +27180,6 @@
|
||||
"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": {
|
||||
"version": "0.1.1",
|
||||
"license": "MIT"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.3.0",
|
||||
"@azure/cosmos": "4.5.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "4.5.0",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[defaults]
|
||||
group = dataexplorer-preview
|
||||
sku = P1V2
|
||||
sku = P1v2
|
||||
appserviceplan = dataexplorer-preview
|
||||
location = westus2
|
||||
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": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2",
|
||||
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:20-lts\" --sku P1V2",
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Microsoft Corporation",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"express": "^4.21.2",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"node": "^18.20.6",
|
||||
"node-fetch": "^2.6.1"
|
||||
"node": "^20.19.5",
|
||||
"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,
|
||||
}
|
||||
|
||||
export class BackendApi {
|
||||
public static readonly GenerateToken: string = "GenerateToken";
|
||||
public static readonly PortalSettings: string = "PortalSettings";
|
||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
||||
public static readonly SampleData: string = "SampleData";
|
||||
export class AadScopeEndpoints {
|
||||
public static readonly Development: string = "https://cosmos.azure.com";
|
||||
public static readonly MPAC: string = "https://cosmos.azure.com";
|
||||
public static readonly Prod: string = "https://cosmos.azure.com";
|
||||
public static readonly Fairfax: string = "https://cosmos.azure.us";
|
||||
public static readonly Mooncake: string = "https://cosmos.azure.cn";
|
||||
}
|
||||
|
||||
export class PortalBackendEndpoints {
|
||||
@@ -264,6 +263,7 @@ export class HttpHeaders {
|
||||
public static activityId: string = "x-ms-activity-id";
|
||||
public static apiType: string = "x-ms-cosmos-apitype";
|
||||
public static authorization: string = "authorization";
|
||||
public static entraIdToken: string = "x-ms-entraid-token";
|
||||
public static collectionIndexTransformationProgress: string =
|
||||
"x-ms-documentdb-collection-index-transformation-progress";
|
||||
public static continuation: string = "x-ms-continuation";
|
||||
@@ -774,3 +774,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
|
||||
|
||||
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 { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { PriorityLevel } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
||||
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||
@@ -20,8 +20,7 @@ const _global = typeof self === "undefined" ? window : self;
|
||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||
|
||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
||||
if (useDataplaneRbacAuthorization(userContext)) {
|
||||
Logger.logInfo(
|
||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||
"Explorer/tokenProvider",
|
||||
|
||||
@@ -28,3 +28,39 @@ describe("Environment Utility Test", () => {
|
||||
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";
|
||||
|
||||
export function normalizeArmEndpoint(uri: string): string {
|
||||
@@ -27,3 +28,17 @@ export const getEnvironment = (): Environment => {
|
||||
|
||||
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({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -84,7 +83,6 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
queryDocuments(databaseId, collection, true, "{}");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
@@ -101,7 +99,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -120,7 +117,6 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
readDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
@@ -137,7 +133,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -156,7 +151,6 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
readDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
@@ -173,7 +167,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -197,7 +190,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -216,7 +208,6 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
deleteDocuments(databaseId, collection, [documentId]);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
@@ -233,7 +224,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import { getMongoGuidRepresentation } from "Shared/StorageUtility";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
@@ -6,6 +7,7 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { userContext } from "../UserContext";
|
||||
import { isDataplaneRbacEnabledForProxyApi } from "../Utils/AuthorizationUtils";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
@@ -21,7 +23,13 @@ function authHeaders() {
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
|
||||
} 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.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
clientSettings: {
|
||||
guidRepresentation: getMongoGuidRepresentation(),
|
||||
},
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
@@ -181,6 +192,9 @@ export function createDocument(
|
||||
partitionKey:
|
||||
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||
documentContent: JSON.stringify(documentContent),
|
||||
clientSettings: {
|
||||
guidRepresentation: getMongoGuidRepresentation(),
|
||||
},
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
@@ -228,6 +242,9 @@ export function updateDocument(
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
documentContent,
|
||||
clientSettings: {
|
||||
guidRepresentation: getMongoGuidRepresentation(),
|
||||
},
|
||||
};
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
@@ -274,6 +291,9 @@ export function deleteDocuments(
|
||||
subscriptionID: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
clientSettings: {
|
||||
guidRepresentation: getMongoGuidRepresentation(),
|
||||
},
|
||||
};
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
||||
{
|
||||
"disableNonStreamingOrderByQuery": true,
|
||||
"enableQueryControl": false,
|
||||
"enableScanInQuery": true,
|
||||
"forceQueryPlan": true,
|
||||
@@ -14,7 +13,6 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] =
|
||||
|
||||
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
||||
{
|
||||
"disableNonStreamingOrderByQuery": true,
|
||||
"enableQueryControl": false,
|
||||
"enableScanInQuery": true,
|
||||
"forceQueryPlan": true,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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 { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
@@ -43,6 +45,14 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
}
|
||||
|
||||
logConsoleInfo(`Successfully created container ${params.collectionId}`);
|
||||
|
||||
if (isFabricNative()) {
|
||||
sendMessage({
|
||||
type: FabricMessageTypes.ContainerUpdated,
|
||||
params: { updateType: "created" },
|
||||
});
|
||||
}
|
||||
|
||||
return collection;
|
||||
} catch (error) {
|
||||
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 { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
@@ -19,6 +21,11 @@ export async function deleteCollection(databaseId: string, collectionId: string)
|
||||
await client().database(databaseId).container(collectionId).delete();
|
||||
}
|
||||
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
||||
|
||||
sendMessage({
|
||||
type: FabricMessageTypes.ContainerUpdated,
|
||||
params: { updateType: "deleted" },
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteCollection", `Error while deleting container ${collectionId}`);
|
||||
throw error;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { userContext } from "../../UserContext";
|
||||
@@ -41,7 +42,7 @@ interface MetricsResponse {
|
||||
}
|
||||
|
||||
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||
if (userContext.authType !== AuthType.AAD) {
|
||||
if (userContext.authType !== AuthType.AAD || isFabricNative()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { Queries } from "../Constants";
|
||||
import { client } from "../CosmosClient";
|
||||
@@ -28,6 +27,5 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
||||
Queries.itemsPerPage;
|
||||
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
|
||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
|
||||
return options;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Item, RequestOptions } from "@azure/cosmos";
|
||||
import { HttpHeaders } from "Common/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
@@ -23,10 +24,17 @@ export const updateDocument = async (
|
||||
[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()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||
.item(documentId.id(), partitionKey)
|
||||
.replace(newDocument, options);
|
||||
|
||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMsalRedirectEndpoints,
|
||||
defaultAllowedAadEndpoints,
|
||||
defaultAllowedArmEndpoints,
|
||||
defaultAllowedBackendEndpoints,
|
||||
defaultAllowedCassandraProxyEndpoints,
|
||||
defaultAllowedGraphEndpoints,
|
||||
defaultAllowedMongoProxyEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
@@ -29,6 +23,8 @@ export enum Platform {
|
||||
|
||||
export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedAadEndpoints: ReadonlyArray<string>;
|
||||
allowedGraphEndpoints: ReadonlyArray<string>;
|
||||
allowedArmEndpoints: ReadonlyArray<string>;
|
||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||
@@ -37,10 +33,8 @@ export interface ConfigContext {
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
AAD_ENDPOINT: string;
|
||||
ARM_AUTH_AREA: string;
|
||||
ARM_ENDPOINT: string;
|
||||
EMULATOR_ENDPOINT?: string;
|
||||
ARM_API_VERSION: string;
|
||||
GRAPH_ENDPOINT: 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
|
||||
@@ -50,27 +44,24 @@ export interface ConfigContext {
|
||||
ARCADIA_ENDPOINT: string;
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||
PORTAL_BACKEND_ENDPOINT: string;
|
||||
NEW_BACKEND_APIS?: BackendApi[];
|
||||
MONGO_PROXY_ENDPOINT: string;
|
||||
CASSANDRA_PROXY_ENDPOINT: string;
|
||||
NEW_CASSANDRA_APIS?: string[];
|
||||
PROXY_PATH?: string;
|
||||
JUNO_ENDPOINT: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
GITHUB_TEST_ENV_CLIENT_ID: string;
|
||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||
isTerminalEnabled: boolean;
|
||||
isPhoenixEnabled: boolean;
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
msalRedirectURI?: string;
|
||||
globallyEnabledCassandraAPIs?: string[];
|
||||
globallyEnabledMongoAPIs?: string[];
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
let configContext: Readonly<ConfigContext> = {
|
||||
platform: Platform.Portal,
|
||||
allowedAadEndpoints: defaultAllowedAadEndpoints,
|
||||
allowedGraphEndpoints: defaultAllowedGraphEndpoints,
|
||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||
@@ -85,17 +76,12 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
||||
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||
ARM_AUTH_AREA: "https://management.azure.com/",
|
||||
ARM_ENDPOINT: "https://management.azure.com/",
|
||||
ARM_API_VERSION: "2016-06-01",
|
||||
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
||||
GRAPH_API_VERSION: "1.6",
|
||||
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
||||
@@ -109,11 +95,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||
isTerminalEnabled: false,
|
||||
isPhoenixEnabled: false,
|
||||
globallyEnabledCassandraAPIs: [],
|
||||
globallyEnabledMongoAPIs: [],
|
||||
};
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
@@ -128,19 +110,38 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
||||
delete newContext.ARM_ENDPOINT;
|
||||
if (newContext.allowedAadEndpoints) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints)) {
|
||||
delete newContext.ARM_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
||||
delete newContext.EMULATOR_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
|
||||
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints)) {
|
||||
delete newContext.GRAPH_ENDPOINT;
|
||||
}
|
||||
|
||||
@@ -148,21 +149,15 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.ARCADIA_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.MONGO_PROXY_ENDPOINT,
|
||||
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
|
||||
)
|
||||
) {
|
||||
if (!validateEndpoint(newContext.PORTAL_BACKEND_ENDPOINT, configContext.allowedBackendEndpoints)) {
|
||||
delete newContext.PORTAL_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, configContext.allowedMongoProxyEndpoints)) {
|
||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
||||
configContext.allowedCassandraProxyEndpoints || defaultAllowedCassandraProxyEndpoints,
|
||||
)
|
||||
) {
|
||||
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, configContext.allowedCassandraProxyEndpoints)) {
|
||||
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FabricMessageTypes } from "./FabricMessageTypes";
|
||||
import { MessageTypes } from "./MessageTypes";
|
||||
|
||||
// This is the current version of these messages
|
||||
export const DATA_EXPLORER_RPC_VERSION = "3";
|
||||
@@ -19,9 +20,32 @@ export type DataExploreMessageV3 =
|
||||
type: FabricMessageTypes.GetAllResourceTokens;
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: FabricMessageTypes.GetAccessToken;
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: MessageTypes.TelemetryInfo;
|
||||
data: {
|
||||
action: string;
|
||||
actionModifier: string;
|
||||
data: unknown;
|
||||
timestamp: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: FabricMessageTypes.OpenSettings;
|
||||
settingsId: string;
|
||||
params: [{ settingsId?: "About" | "Connection" }];
|
||||
}
|
||||
| {
|
||||
type: FabricMessageTypes.RestoreContainer;
|
||||
params: [];
|
||||
}
|
||||
| {
|
||||
type: FabricMessageTypes.ContainerUpdated;
|
||||
params: {
|
||||
updateType: "created" | "deleted" | "settings";
|
||||
};
|
||||
};
|
||||
export interface GetCosmosTokenMessageOptions {
|
||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
||||
|
||||
@@ -389,7 +389,7 @@ export interface VectorEmbeddingPolicy {
|
||||
}
|
||||
|
||||
export interface VectorEmbedding {
|
||||
dataType: "float32" | "uint8" | "int8";
|
||||
dataType: "float32" | "float16" | "uint8" | "int8";
|
||||
dimensions: number;
|
||||
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
||||
path: string;
|
||||
|
||||
@@ -7,6 +7,8 @@ export enum FabricMessageTypes {
|
||||
GetAccessToken = "GetAccessToken",
|
||||
Ready = "Ready",
|
||||
OpenSettings = "OpenSettings",
|
||||
RestoreContainer = "RestoreContainer",
|
||||
ContainerUpdated = "ContainerUpdated",
|
||||
}
|
||||
|
||||
export interface AuthorizationToken {
|
||||
|
||||
@@ -443,6 +443,7 @@ export interface DataExplorerInputsFrame {
|
||||
[key: string]: string;
|
||||
};
|
||||
feedbackPolicies?: any;
|
||||
aadToken?: string;
|
||||
}
|
||||
|
||||
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;
|
||||
discardChanges?: boolean;
|
||||
onChangesDiscarded?: () => void;
|
||||
englishOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface FullTextPolicyData {
|
||||
@@ -66,6 +67,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
||||
onFullTextPathChange,
|
||||
discardChanges,
|
||||
onChangesDiscarded,
|
||||
englishOnly,
|
||||
}): JSX.Element => {
|
||||
const getFullTextPathError = (path: string, index?: number): string => {
|
||||
let error = "";
|
||||
@@ -87,6 +89,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
||||
if (!fullTextPolicy) {
|
||||
fullTextPolicy = { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] };
|
||||
}
|
||||
|
||||
return fullTextPolicy.fullTextPaths.map((fullTextPath: FullTextPath) => ({
|
||||
...fullTextPath,
|
||||
pathError: getFullTextPathError(fullTextPath.path),
|
||||
@@ -166,7 +169,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getFullTextLanguageOptions()}
|
||||
options={getFullTextLanguageOptions(englishOnly)}
|
||||
selectedKey={defaultLanguage}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
setDefaultLanguage(option.key as never)
|
||||
@@ -211,7 +214,7 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getFullTextLanguageOptions()}
|
||||
options={getFullTextLanguageOptions(englishOnly)}
|
||||
selectedKey={fullTextPolicy.language}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onFullTextPathPolicyChange(index, option)
|
||||
@@ -229,11 +232,25 @@ export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPolicies
|
||||
);
|
||||
};
|
||||
|
||||
export const getFullTextLanguageOptions = (): IDropdownOption[] => {
|
||||
return [
|
||||
export const getFullTextLanguageOptions = (englishOnly?: boolean): IDropdownOption[] => {
|
||||
const fullTextLanguageOptions: IDropdownOption[] = [
|
||||
{
|
||||
key: "en-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 { sendMessage } from "Common/MessageHandler";
|
||||
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||
import {
|
||||
ComputedPropertiesComponent,
|
||||
ComputedPropertiesComponentProps,
|
||||
@@ -431,6 +433,15 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
);
|
||||
} finally {
|
||||
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 => (
|
||||
<>
|
||||
{this.props.isAutoPilotSelected ? (
|
||||
<TextField
|
||||
label="Maximum RU/s required by this resource"
|
||||
required
|
||||
type="number"
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
||||
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 horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
|
||||
{/* Column 1: Minimum RU/s */}
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||
Minimum RU/s
|
||||
</Text>
|
||||
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
|
||||
</Stack>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "Segoe UI",
|
||||
width: 70,
|
||||
height: 28,
|
||||
border: "none",
|
||||
fontSize: 14,
|
||||
backgroundColor: "transparent",
|
||||
fontWeight: 400,
|
||||
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
|
||||
required
|
||||
|
||||
@@ -157,35 +157,148 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
disabled={true}
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
label="Maximum RU/s required by this resource"
|
||||
min={1000}
|
||||
onChange={[Function]}
|
||||
onGetErrorMessage={[Function]}
|
||||
required={true}
|
||||
step={1000}
|
||||
styles={
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
{
|
||||
"fieldGroup": {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": {
|
||||
":disabled": {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
"childrenGap": 8,
|
||||
}
|
||||
}
|
||||
type="number"
|
||||
validateOnLoad={false}
|
||||
value=""
|
||||
/>
|
||||
verticalAlign="end"
|
||||
>
|
||||
<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
|
||||
|
||||
@@ -5,13 +5,13 @@ import { useDatabases } from "Explorer/useDatabases";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||
import "./ThroughputInput.less";
|
||||
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
||||
|
||||
export interface ThroughputInputProps {
|
||||
isDatabase: boolean;
|
||||
@@ -41,11 +41,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
let defaultThroughput: number;
|
||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
||||
|
||||
if (
|
||||
if (isFabricNative()) {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput5K;
|
||||
} else if (
|
||||
isFreeTier ||
|
||||
isQuickstart ||
|
||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType) ||
|
||||
isFabricNative()
|
||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
||||
) {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
||||
} else if (workloadType === Constants.WorkloadType.Production) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IDropdownOption } from "@fluentui/react";
|
||||
|
||||
const dataTypes = ["float32", "uint8", "int8"];
|
||||
const dataTypes = ["float32", "float16", "uint8", "int8"];
|
||||
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
||||
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
|
||||
@@ -90,12 +91,13 @@ export class ContainerSampleGenerator {
|
||||
}
|
||||
const { databaseAccount: account } = userContext;
|
||||
const databaseId = collection.databaseId;
|
||||
|
||||
const gremlinClient = new GremlinClient();
|
||||
gremlinClient.initialize({
|
||||
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
|
||||
databaseId: databaseId,
|
||||
collectionId: collection.id(),
|
||||
masterKey: userContext.masterKey || "",
|
||||
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||
maxResultSize: 100,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,12 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
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 { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
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 resourceId = encodeURIComponent(userContext.databaseAccount.id);
|
||||
const database = encodeURIComponent(activeTab?.collection?.databaseId);
|
||||
const container = encodeURIComponent(activeTab?.collection?.id());
|
||||
const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
|
||||
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 = {
|
||||
linkProps: {
|
||||
linkText: "Download Visual Studio Code",
|
||||
@@ -1149,7 +1180,10 @@ export default class Explorer {
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: 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
|
||||
const isNotebookEnabled =
|
||||
@@ -1171,7 +1205,7 @@ export default class Explorer {
|
||||
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");
|
||||
updateUserContext({ throughputBucketsEnabled });
|
||||
}
|
||||
|
||||
@@ -163,8 +163,7 @@ describe("GraphExplorer", () => {
|
||||
graphBackendEndpoint: "graphBackendEndpoint",
|
||||
databaseId: "databaseId",
|
||||
collectionId: "collectionId",
|
||||
masterKey: "masterKey",
|
||||
|
||||
password: "password",
|
||||
onLoadStartKey: 0,
|
||||
onLoadStartKeyChange: (newKey: number): void => {},
|
||||
resourceId: "resourceId",
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface GraphExplorerProps {
|
||||
graphBackendEndpoint: string;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
masterKey: string;
|
||||
password: string;
|
||||
|
||||
onLoadStartKey: number;
|
||||
onLoadStartKeyChange: (newKey: number) => void;
|
||||
@@ -1300,7 +1300,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
endpoint: `wss://${this.props.graphBackendEndpoint}`,
|
||||
databaseId: this.props.databaseId,
|
||||
collectionId: this.props.collectionId,
|
||||
masterKey: this.props.masterKey,
|
||||
password: this.props.password,
|
||||
maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,28 +8,28 @@ describe("Gremlin Client", () => {
|
||||
endpoint: null,
|
||||
collectionId: null,
|
||||
databaseId: null,
|
||||
masterKey: null,
|
||||
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 databaseId = "databaseId";
|
||||
const masterKey = "masterKey";
|
||||
const testPassword = "password";
|
||||
const gremlinClient = new GremlinClient();
|
||||
|
||||
gremlinClient.initialize({
|
||||
endpoint: null,
|
||||
collectionId,
|
||||
databaseId,
|
||||
masterKey,
|
||||
maxResultSize: 0,
|
||||
password: testPassword,
|
||||
});
|
||||
|
||||
// User must includes these values
|
||||
expect(gremlinClient.client.params.user.indexOf(collectionId)).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) => {
|
||||
|
||||
@@ -11,8 +11,8 @@ export interface GremlinClientParameters {
|
||||
endpoint: string;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
masterKey: string;
|
||||
maxResultSize: number;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface GremlinRequestResult {
|
||||
@@ -43,7 +43,7 @@ export class GremlinClient {
|
||||
this.client = new GremlinSimpleClient({
|
||||
endpoint: params.endpoint,
|
||||
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
|
||||
password: params.masterKey,
|
||||
password: params.password,
|
||||
successCallback: (result: Result) => {
|
||||
this.storePendingResult(result);
|
||||
this.flushResult(result.requestId);
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
import * as sinon from "sinon";
|
||||
import {
|
||||
GremlinRequestMessage,
|
||||
GremlinResponseMessage,
|
||||
GremlinSimpleClient,
|
||||
GremlinSimpleClientParameters,
|
||||
Result,
|
||||
GremlinRequestMessage,
|
||||
GremlinResponseMessage,
|
||||
} from "./GremlinSimpleClient";
|
||||
|
||||
describe("Gremlin Simple Client", () => {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
@@ -30,7 +31,7 @@ export interface CommandBarStore {
|
||||
}
|
||||
|
||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
||||
contextButtons: [],
|
||||
contextButtons: [] as CommandButtonComponentProps[],
|
||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||
isHidden: false,
|
||||
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
||||
@@ -43,6 +44,15 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
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") {
|
||||
const buttons =
|
||||
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(
|
||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
||||
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
||||
@@ -68,15 +67,7 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
|
||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||
|
||||
useEffect(() => {
|
||||
const buttonProps = createLoginForEntraIDButton(container);
|
||||
setLoginButtonProps(buttonProps);
|
||||
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
|
||||
|
||||
const loginButtonProps = createLoginForEntraIDButton(container);
|
||||
if (loginButtonProps) {
|
||||
addDivider();
|
||||
buttons.push(loginButtonProps);
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
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 { CollectionCreation } from "Shared/Constants";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
@@ -60,7 +60,6 @@ import { useDatabases } from "../../useDatabases";
|
||||
import { PanelFooterComponent } from "../PanelFooterComponent";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
|
||||
export interface AddCollectionPanelProps {
|
||||
explorer: Explorer;
|
||||
@@ -123,7 +122,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
isSharded: userContext.apiType !== "Tables",
|
||||
partitionKey: getPartitionKey(props.isQuickstart),
|
||||
subPartitionKeys: [],
|
||||
enableDedicatedThroughput: false,
|
||||
enableDedicatedThroughput: isFabricNative(), // Dedicated throughput is only enabled in Fabric Native by default
|
||||
createMongoWildCardIndex:
|
||||
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
||||
useHashV1: false,
|
||||
@@ -406,9 +405,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
responsiveMode={999}
|
||||
/>
|
||||
)}
|
||||
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
|
||||
</Stack>
|
||||
)}
|
||||
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
|
||||
|
||||
<Stack>
|
||||
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
|
||||
@@ -448,8 +447,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
this.setState({ collectionId: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
|
||||
</Stack>
|
||||
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
|
||||
|
||||
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
|
||||
<Stack>
|
||||
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
|
||||
@@ -644,7 +644,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<DefaultButton
|
||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||
@@ -708,7 +708,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{this.shouldShowCollectionThroughputInput() && (
|
||||
{this.shouldShowCollectionThroughputInput() && !isFabricNative() && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={false}
|
||||
@@ -775,7 +775,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
|
||||
)}
|
||||
|
||||
{shouldShowAnalyticalStoreOptions() && (
|
||||
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
|
||||
@@ -891,6 +893,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
) => {
|
||||
this.setState({ fullTextPolicy, fullTextIndexes, fullTextPolicyValidated });
|
||||
}}
|
||||
englishOnly={true}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -1131,7 +1134,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
// }
|
||||
|
||||
private shouldShowCollectionThroughputInput(): boolean {
|
||||
if (isFabricNative() || isServerlessAccount()) {
|
||||
if (isServerlessAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1352,8 +1355,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
// Throughput
|
||||
if (isFabricNative()) {
|
||||
// Fabric Native accounts are always autoscale and have a fixed throughput of 1K
|
||||
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
||||
autoPilotMaxThroughput = DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT;
|
||||
offerThroughput = undefined;
|
||||
} else if (databaseLevelThroughput) {
|
||||
if (this.state.createNewDatabase) {
|
||||
|
||||
@@ -142,16 +142,16 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
style={
|
||||
{
|
||||
"marginBottom": -4,
|
||||
"marginTop": -4,
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
style={
|
||||
{
|
||||
"marginBottom": -4,
|
||||
"marginTop": -4,
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
@@ -202,16 +202,16 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</Stack>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
style={
|
||||
{
|
||||
"marginBottom": -5,
|
||||
"marginTop": -5,
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
style={
|
||||
{
|
||||
"marginBottom": -5,
|
||||
"marginTop": -5,
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
|
||||
@@ -199,6 +199,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
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 explorerVersion = configContext.gitSha;
|
||||
@@ -261,6 +270,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||
!isEmulator;
|
||||
|
||||
const shouldShowMongoGuidRepresentationOption = userContext.apiType === "Mongo";
|
||||
|
||||
const handlerOnSubmit = async () => {
|
||||
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);
|
||||
logConsoleInfo(
|
||||
`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(
|
||||
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
||||
`${ignorePartitionKeyOnDocumentUpdate ? "Enabled" : "Disabled"} ignoring partition key on document update`,
|
||||
);
|
||||
|
||||
refreshExplorer && (await explorer.refreshExplorer());
|
||||
closeSidePanel();
|
||||
};
|
||||
@@ -480,6 +510,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{ 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 = (
|
||||
ev: React.FormEvent<HTMLInputElement>,
|
||||
option: IChoiceGroupOption,
|
||||
@@ -562,6 +599,20 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
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 = {
|
||||
root: {
|
||||
clear: "both",
|
||||
@@ -1068,15 +1119,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
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
|
||||
is created by, and maintained by Microsoft at no cost to you.
|
||||
NoSQL queries. This will appear as another database in the Data Explorer UI, and is created by,
|
||||
and maintained by Microsoft at no cost to you.
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable sample db for Query Advisor"
|
||||
ariaLabel="Enable sample db for query exploration"
|
||||
checked={copilotSampleDBEnabled}
|
||||
onChange={handleSampleDatabaseChange}
|
||||
label="Enable sample database"
|
||||
@@ -1085,6 +1136,44 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -575,6 +575,37 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</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>
|
||||
<div
|
||||
className="settingsSection"
|
||||
@@ -838,6 +869,37 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</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>
|
||||
<div
|
||||
className="settingsSection"
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
@@ -13,7 +11,6 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
|
||||
import { userContext } from "UserContext";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
|
||||
import React, { useState } from "react";
|
||||
import SplitterLayout from "react-splitter-layout";
|
||||
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
||||
@@ -26,7 +23,8 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
||||
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 executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||
@@ -70,17 +68,18 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||
}, [query, selectedQuery, copilotActive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
useTabs.subscribe((state: TabsState) => {
|
||||
if (state.activeReactTab === ReactTabKind.QueryCopilot) {
|
||||
setTabActive(true);
|
||||
} else {
|
||||
setTabActive(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
//TODO: Uncomment this effect when query copilot is reinstated in DE
|
||||
// React.useEffect(() => {
|
||||
// return () => {
|
||||
// useTabs.subscribe((state: TabsState) => {
|
||||
// if (state.activeReactTab === ReactTabKind.QueryCopilot) {
|
||||
// setTabActive(true);
|
||||
// } else {
|
||||
// setTabActive(false);
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
const toggleCopilot = (toggle: boolean) => {
|
||||
setCopilotActive(toggle);
|
||||
@@ -90,6 +89,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
return (
|
||||
<Stack className="tab-pane" style={{ width: "100%" }}>
|
||||
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
||||
{/*TODO: Uncomment this section when query copilot is reinstated in DE
|
||||
{tabActive && copilotActive && (
|
||||
<QueryCopilotPromptbar
|
||||
explorer={explorer}
|
||||
@@ -97,7 +97,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
databaseId={QueryCopilotSampleDatabaseId}
|
||||
containerId={QueryCopilotSampleContainerId}
|
||||
></QueryCopilotPromptbar>
|
||||
)}
|
||||
)} */}
|
||||
<Stack className="tabPaneContentContainer">
|
||||
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
||||
<EditorReact
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
/**
|
||||
* Accordion top class
|
||||
*/
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
||||
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
||||
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { DocumentAddRegular, LinkMultipleRegular, OpenRegular } from "@fluentui/react-icons";
|
||||
import { SampleDataConfiguration, SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
||||
import { SampleDataFile } from "Explorer/SplashScreen/SampleUtil";
|
||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
||||
import * as React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import AzureOpenAiIcon from "../../../images/AzureOpenAi.svg";
|
||||
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
|
||||
import GithubIcon from "../../../images/github-black-and-white.svg";
|
||||
import Explorer from "../Explorer";
|
||||
|
||||
export interface SplashScreenProps {
|
||||
@@ -26,11 +29,11 @@ const useStyles = makeStyles({
|
||||
fontWeight: "bold",
|
||||
},
|
||||
buttonsContainer: {
|
||||
width: "584px",
|
||||
width: "760px",
|
||||
margin: "auto",
|
||||
display: "grid",
|
||||
padding: "16px",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
gap: "10px",
|
||||
gridAutoRows: "minmax(184px, auto)",
|
||||
},
|
||||
@@ -53,6 +56,15 @@ const useStyles = makeStyles({
|
||||
},
|
||||
},
|
||||
three: {
|
||||
gridColumn: "4",
|
||||
gridRow: "1",
|
||||
"& img": {
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
margin: "auto",
|
||||
},
|
||||
},
|
||||
four: {
|
||||
gridColumn: "3",
|
||||
gridRow: "2",
|
||||
"& svg": {
|
||||
@@ -61,6 +73,15 @@ const useStyles = makeStyles({
|
||||
margin: "auto",
|
||||
},
|
||||
},
|
||||
five: {
|
||||
gridColumn: "4",
|
||||
gridRow: "2",
|
||||
"& img": {
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
margin: "auto",
|
||||
},
|
||||
},
|
||||
single: {
|
||||
gridColumn: "1 / 4",
|
||||
gridRow: "1 / 3",
|
||||
@@ -119,7 +140,7 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
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 aria-label={title} className={styles.buttonLowerPart}>
|
||||
<div>{title}</div>
|
||||
@@ -132,6 +153,8 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
|
||||
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
|
||||
const styles = useStyles();
|
||||
const [openSampleDataImportDialog, setOpenSampleDataImportDialog] = React.useState(false);
|
||||
const [selectedSampleDataConfiguration, setSelectedSampleDataConfiguration] =
|
||||
React.useState<SampleDataConfiguration>(undefined);
|
||||
|
||||
const getSplashScreenButtons = (): JSX.Element => {
|
||||
const buttons: FabricHomeScreenButtonProps[] = [
|
||||
@@ -145,10 +168,30 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Sample data",
|
||||
description: "Automatically load sample data in your database",
|
||||
icon: <img src={CosmosDbBlackIcon} />,
|
||||
onClick: () => setOpenSampleDataImportDialog(true),
|
||||
title: "Sample Data",
|
||||
description: "Load sample data in your database",
|
||||
icon: <img src={CosmosDbBlackIcon} alt={"Azure Cosmos DB icon"} aria-hidden="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",
|
||||
@@ -156,17 +199,25 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
icon: <LinkMultipleRegular />,
|
||||
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() ? (
|
||||
<div className={styles.buttonsContainer}>
|
||||
<FabricHomeScreenButton className={styles.single} {...buttons[2]} />
|
||||
<FabricHomeScreenButton className={styles.single} {...buttons[3]} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.buttonsContainer}>
|
||||
<FabricHomeScreenButton className={styles.one} {...buttons[0]} />
|
||||
<FabricHomeScreenButton className={styles.two} {...buttons[1]} />
|
||||
<FabricHomeScreenButton className={styles.three} {...buttons[2]} />
|
||||
<FabricHomeScreenButton className={styles.four} {...buttons[3]} />
|
||||
<FabricHomeScreenButton className={styles.five} {...buttons[4]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -179,18 +230,20 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
open={openSampleDataImportDialog}
|
||||
setOpen={setOpenSampleDataImportDialog}
|
||||
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}
|
||||
</div>
|
||||
{getSplashScreenButtons()}
|
||||
{/* <div className={styles.footer}>
|
||||
Need help?{" "}
|
||||
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
|
||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
||||
</Link>
|
||||
</div> */}
|
||||
{
|
||||
<div className={styles.footer}>
|
||||
Need help?{" "}
|
||||
<Link href="https://learn.microsoft.com/fabric/database/cosmos-db/overview" target="_blank">
|
||||
Learn more <OpenRegular />
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
</CosmosFluentProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -11,12 +11,10 @@ import {
|
||||
tokens,
|
||||
} from "@fluentui/react-components";
|
||||
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 * as ViewModels from "../../Contracts/ViewModels";
|
||||
|
||||
const SAMPLE_DATA_CONTAINER_NAME = "SampleData";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
dialogContent: {
|
||||
alignItems: "center",
|
||||
@@ -24,6 +22,12 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
export interface SampleDataConfiguration {
|
||||
databaseName: string;
|
||||
newContainerName: string;
|
||||
sampleDataFile: SampleDataFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* This dialog:
|
||||
* - creates a container
|
||||
@@ -35,11 +39,11 @@ export const SampleDataImportDialog: React.FC<{
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
explorer: Explorer;
|
||||
databaseName: string;
|
||||
sampleDataConfiguration: SampleDataConfiguration | undefined;
|
||||
}> = (props) => {
|
||||
const [status, setStatus] = useState<"idle" | "creating" | "importing" | "completed" | "error">("idle");
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const containerName = SAMPLE_DATA_CONTAINER_NAME;
|
||||
const containerName = props.sampleDataConfiguration?.newContainerName;
|
||||
const [collection, setCollection] = useState<ViewModels.Collection>(undefined);
|
||||
const styles = useStyles();
|
||||
|
||||
@@ -53,7 +57,7 @@ export const SampleDataImportDialog: React.FC<{
|
||||
|
||||
const handleStartImport = async (): Promise<void> => {
|
||||
setStatus("creating");
|
||||
const databaseName = props.databaseName;
|
||||
const databaseName = props.sampleDataConfiguration.databaseName;
|
||||
if (checkContainerExists(databaseName, containerName)) {
|
||||
const msg = `The container "${containerName}" in database "${databaseName}" already exists. Please delete it and retry.`;
|
||||
setStatus("error");
|
||||
@@ -63,7 +67,12 @@ export const SampleDataImportDialog: React.FC<{
|
||||
|
||||
let collection;
|
||||
try {
|
||||
collection = await createContainer(databaseName, containerName, props.explorer);
|
||||
collection = await createContainer(
|
||||
databaseName,
|
||||
containerName,
|
||||
props.explorer,
|
||||
props.sampleDataConfiguration.sampleDataFile,
|
||||
);
|
||||
} catch (error) {
|
||||
setStatus("error");
|
||||
setErrorMessage(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`);
|
||||
@@ -72,7 +81,7 @@ export const SampleDataImportDialog: React.FC<{
|
||||
|
||||
try {
|
||||
setStatus("importing");
|
||||
await importData(collection);
|
||||
await importData(props.sampleDataConfiguration.sampleDataFile, collection);
|
||||
setCollection(collection);
|
||||
setStatus("completed");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { JSONObject } from "@azure/cosmos";
|
||||
import { BackendDefaults } from "Common/Constants";
|
||||
import { createCollection } from "Common/dataAccess/createCollection";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
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 ViewModels from "../../Contracts/ViewModels";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
/**
|
||||
* Public for unit tests
|
||||
@@ -26,12 +30,20 @@ const hasContainer = (
|
||||
export const checkContainerExists = (databaseName: string, containerName: string) =>
|
||||
hasContainer(databaseName, containerName, useDatabases.getState().databases);
|
||||
|
||||
export enum SampleDataFile {
|
||||
COPILOT = "Copilot",
|
||||
FABRIC_SAMPLE_DATA = "FabricSampleData",
|
||||
FABRIC_SAMPLE_VECTOR_DATA = "FabricSampleVectorData",
|
||||
}
|
||||
|
||||
export const createContainer = async (
|
||||
databaseName: string,
|
||||
containerName: string,
|
||||
explorer: Explorer,
|
||||
sampleDataFile: SampleDataFile,
|
||||
): Promise<ViewModels.Collection> => {
|
||||
const createRequest: DataModels.CreateCollectionParams = {
|
||||
autoPilotMaxThroughput: isFabricNative() ? DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT : undefined,
|
||||
createNewDatabase: false,
|
||||
collectionId: containerName,
|
||||
databaseId: databaseName,
|
||||
@@ -41,6 +53,44 @@ export const createContainer = async (
|
||||
kind: "Hash",
|
||||
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 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
|
||||
|
||||
export const importData = async (collection: ViewModels.Collection): Promise<void> => {
|
||||
// TODO: keep same chunk as ContainerSampleGenerator
|
||||
const dataFileContent = await import(
|
||||
/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json"
|
||||
);
|
||||
await collection.bulkInsertDocuments(dataFileContent.data);
|
||||
export const importData = async (sampleDataFile: SampleDataFile, collection: ViewModels.Collection): Promise<void> => {
|
||||
let documents: JSONObject[] = undefined;
|
||||
switch (sampleDataFile) {
|
||||
case SampleDataFile.COPILOT:
|
||||
documents = (
|
||||
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 ConnectIcon from "../../../images/Connect_color.svg";
|
||||
import ContainersIcon from "../../../images/Containers.svg";
|
||||
import CosmosDBIcon from "../../../images/CosmosDB-logo.svg";
|
||||
import LinkIcon from "../../../images/Link_blue.svg";
|
||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||
@@ -120,11 +121,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
};
|
||||
|
||||
private getSplashScreenButtons = (): JSX.Element => {
|
||||
if (
|
||||
userContext.apiType === "SQL" &&
|
||||
useQueryCopilot.getState().copilotEnabled &&
|
||||
useDatabases.getState().sampleDataResourceTokenCollection
|
||||
) {
|
||||
if (userContext.apiType === "SQL") {
|
||||
return (
|
||||
<Stack
|
||||
className="splashStackContainer"
|
||||
@@ -152,25 +149,18 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
/>
|
||||
</Stack>
|
||||
<Stack className="splashStackRow" horizontal>
|
||||
{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 });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SplashScreenButton
|
||||
imgSrc={CosmosDBIcon}
|
||||
imgSize={35}
|
||||
title={"Azure Cosmos DB Samples Gallery"}
|
||||
description={
|
||||
"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={() => {
|
||||
window.open("https://azurecosmosdb.github.io/gallery/?tags=example", "_blank");
|
||||
traceOpen(Action.LearningResourcesClicked, { apiType: userContext.apiType });
|
||||
}}
|
||||
/>
|
||||
<SplashScreenButton
|
||||
imgSrc={ConnectIcon}
|
||||
title={"Connect"}
|
||||
@@ -212,6 +202,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
sample data, query.
|
||||
</TeachingBubble>
|
||||
)}
|
||||
{/*TODO: convert below to use SplashScreenButton */}
|
||||
{mainItems.map((item) => (
|
||||
<Stack
|
||||
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) {
|
||||
return {
|
||||
iconSrc: CollectionIcon,
|
||||
|
||||
@@ -7,6 +7,7 @@ interface SplashScreenButtonProps {
|
||||
title: string;
|
||||
description: string;
|
||||
onClick: () => void;
|
||||
imgSize?: number;
|
||||
}
|
||||
|
||||
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||
@@ -14,6 +15,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||
title,
|
||||
description,
|
||||
onClick,
|
||||
imgSize,
|
||||
}: SplashScreenButtonProps): JSX.Element => {
|
||||
return (
|
||||
<Stack
|
||||
@@ -39,7 +41,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||
role="button"
|
||||
>
|
||||
<div>
|
||||
<img src={imgSrc} alt={title} aria-hidden="true" />
|
||||
<img src={imgSrc} alt={title} aria-hidden="true" {...(imgSize ? { height: imgSize, width: imgSize } : {})} />
|
||||
</div>
|
||||
<Stack style={{ marginLeft: 16 }}>
|
||||
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import { getAuthorizationHeader, isDataplaneRbacEnabledForProxyApi } from "../../Utils/AuthorizationUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
@@ -551,6 +551,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
|
||||
|
||||
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||
xhr.setRequestHeader(Constants.HttpHeaders.entraIdToken, userContext.aadToken);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { formatErrorMessage, formatInfoMessage, formatWarningMessage } from "./U
|
||||
|
||||
// Constants
|
||||
const DEFAULT_CLOUDSHELL_REGION = "westus";
|
||||
const DEFAULT_FAIRFAX_CLOUDSHELL_REGION = "usgovvirginia";
|
||||
const POLLING_INTERVAL_MS = 2000;
|
||||
const MAX_RETRY_COUNT = 10;
|
||||
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();
|
||||
|
||||
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
|
||||
terminal.writeln(
|
||||
formatInfoMessage(
|
||||
"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(" 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(
|
||||
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("\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");
|
||||
|
||||
// Ask for user consent for region
|
||||
@@ -159,7 +154,9 @@ export const ensureCloudShellProviderRegistered = async (): Promise<void> => {
|
||||
* Determines the appropriate CloudShell region
|
||||
*/
|
||||
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 requirements may not be fully satisfied when using CloudShell due to limited regional availability. CloudShell services are currently available in the following regions:
|
||||
|
||||
| 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 |
|
||||
Data residency requirements may not be fully satisfied when using CloudShell due to limited regional availability.
|
||||
|
||||
**Note:** For up-to-date supported regions, refer to the region configuration in:
|
||||
`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
|
||||
class MockShellHandler extends AbstractShellHandler {
|
||||
@@ -18,8 +18,8 @@ class MockShellHandler extends AbstractShellHandler {
|
||||
return "mock-endpoint";
|
||||
}
|
||||
|
||||
getTerminalSuppressedData(): string {
|
||||
return "suppressed-data";
|
||||
getTerminalSuppressedData(): string[] {
|
||||
return ["suppressed-data"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("AbstractShellHandler", () => {
|
||||
});
|
||||
|
||||
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.
|
||||
* 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
|
||||
@@ -31,7 +42,16 @@ export abstract class AbstractShellHandler {
|
||||
abstract getShellName(): string;
|
||||
abstract getSetUpCommands(): 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.
|
||||
@@ -57,7 +77,7 @@ export abstract class AbstractShellHandler {
|
||||
START_MARKER,
|
||||
DISABLE_HISTORY,
|
||||
...setupCommands,
|
||||
`{ ${connectionCommand}; } || true;${EXIT_COMMAND}`,
|
||||
`{ ${connectionCommand}; } || true;${this.getExitCommand()}`,
|
||||
];
|
||||
|
||||
return allCommands.join("\n").concat("\n");
|
||||
@@ -77,7 +97,7 @@ export abstract class AbstractShellHandler {
|
||||
* is not already present in the environment.
|
||||
*/
|
||||
protected mongoShellSetupCommands(): string[] {
|
||||
const PACKAGE_VERSION: string = "2.5.0";
|
||||
const PACKAGE_VERSION: string = "2.5.6";
|
||||
return [
|
||||
"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`,
|
||||
@@ -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 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",
|
||||
"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", () => {
|
||||
expect(handler.getTerminalSuppressedData()).toBe("");
|
||||
expect(handler.getTerminalSuppressedData()).toEqual([""]);
|
||||
});
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
public getTerminalSuppressedData(): string {
|
||||
return "";
|
||||
public getTerminalSuppressedData(): string[] {
|
||||
return [""];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ interface DatabaseAccount {
|
||||
|
||||
interface UserContextType {
|
||||
databaseAccount: DatabaseAccount;
|
||||
features: {
|
||||
enableAadDataPlane: boolean;
|
||||
};
|
||||
apiType: string;
|
||||
dataPlaneRbacEnabled: boolean;
|
||||
aadToken?: string;
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
@@ -29,10 +35,13 @@ jest.mock("../../../../UserContext", () => ({
|
||||
mongoEndpoint: "https://test-mongo.documents.azure.com:443/",
|
||||
},
|
||||
},
|
||||
features: { enableAadDataPlane: false },
|
||||
apiType: "Mongo",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../Utils/CommonUtils", () => ({
|
||||
...jest.requireActual("../Utils/CommonUtils"),
|
||||
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
|
||||
}));
|
||||
|
||||
@@ -69,7 +78,7 @@ describe("MongoShellHandler", () => {
|
||||
|
||||
expect(Array.isArray(commands)).toBe(true);
|
||||
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",
|
||||
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
|
||||
};
|
||||
(userContext as UserContextType).dataPlaneRbacEnabled = false;
|
||||
|
||||
const command = mongoShellHandler.getConnectionCommand();
|
||||
|
||||
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/");
|
||||
|
||||
@@ -114,17 +124,55 @@ describe("MongoShellHandler", () => {
|
||||
};
|
||||
|
||||
const command = mongoShellHandler.getConnectionCommand();
|
||||
|
||||
expect(command).toBe("echo 'Database name not found.'");
|
||||
|
||||
// Restore original
|
||||
(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", () => {
|
||||
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 { getHostFromUrl } from "../Utils/CommonUtils";
|
||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
|
||||
import { filterAndCleanTerminalOutput, getHostFromUrl, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
|
||||
|
||||
export class MongoShellHandler extends AbstractShellHandler {
|
||||
private _key: string;
|
||||
private _endpoint: string | undefined;
|
||||
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
|
||||
private _isEntraIdEnabled: boolean = isDataplaneRbacEnabledForProxyApi(userContext);
|
||||
constructor(private key: string) {
|
||||
super();
|
||||
this._key = key;
|
||||
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 {
|
||||
return "MongoDB";
|
||||
}
|
||||
@@ -28,20 +41,22 @@ export class MongoShellHandler extends AbstractShellHandler {
|
||||
if (!dbName) {
|
||||
return "echo 'Database name not found.'";
|
||||
}
|
||||
return (
|
||||
"mongosh mongodb://" +
|
||||
getHostFromUrl(this._endpoint) +
|
||||
":10255?appName=" +
|
||||
this.APP_NAME +
|
||||
" --username " +
|
||||
dbName +
|
||||
" --password " +
|
||||
this._key +
|
||||
" --tls --tlsAllowInvalidCertificates"
|
||||
);
|
||||
const connectionCommand = this._isEntraIdEnabled
|
||||
? this._getAadConnectionCommand(dbName)
|
||||
: this._getKeyConnectionCommand(dbName);
|
||||
const fullCommand = `${DISABLE_TELEMETRY_COMMAND}; ${connectionCommand}`;
|
||||
return fullCommand;
|
||||
}
|
||||
|
||||
public getTerminalSuppressedData(): string {
|
||||
return "Warning: Non-Genuine MongoDB Detected";
|
||||
public getTerminalSuppressedData(): string[] {
|
||||
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", () => {
|
||||
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}`;
|
||||
}
|
||||
|
||||
public getTerminalSuppressedData(): string {
|
||||
return "";
|
||||
public getTerminalSuppressedData(): string[] {
|
||||
return [""];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,24 @@ import { PostgresShellHandler } from "./PostgresShellHandler";
|
||||
import { getHandler, getKey } from "./ShellTypeFactory";
|
||||
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
||||
|
||||
interface UserContextType {
|
||||
databaseAccount: { name: string };
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
features: { enableAadDataPlane: boolean };
|
||||
dataPlaneRbacEnabled: boolean;
|
||||
aadToken?: string;
|
||||
apiType?: string;
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock("../../../../UserContext", () => ({
|
||||
userContext: {
|
||||
databaseAccount: { name: "testDbName" },
|
||||
subscriptionId: "testSubId",
|
||||
resourceGroup: "testResourceGroup",
|
||||
features: { enableAadDataPlane: false },
|
||||
dataPlaneRbacEnabled: false,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -109,5 +121,33 @@ describe("ShellTypeHandlerFactory", () => {
|
||||
expect(key).toBe(mockKey);
|
||||
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 { userContext } from "../../../../UserContext";
|
||||
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
|
||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||
import { CassandraShellHandler } from "./CassandraShellHandler";
|
||||
import { MongoShellHandler } from "./MongoShellHandler";
|
||||
@@ -30,6 +31,9 @@ export async function getKey(): Promise<string> {
|
||||
if (!dbName) {
|
||||
return "";
|
||||
}
|
||||
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||
return userContext.aadToken || "";
|
||||
}
|
||||
|
||||
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
||||
return keys?.primaryMasterKey || "";
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("VCoreMongoShellHandler", () => {
|
||||
|
||||
expect(Array.isArray(commands)).toBe(true);
|
||||
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");
|
||||
});
|
||||
|
||||
@@ -57,7 +57,10 @@ describe("VCoreMongoShellHandler", () => {
|
||||
});
|
||||
|
||||
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 { AbstractShellHandler } from "./AbstractShellHandler";
|
||||
import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
|
||||
|
||||
export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||
private _endpoint: string | undefined;
|
||||
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -23,10 +25,24 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||
}
|
||||
|
||||
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 {
|
||||
return "Warning: Non-Genuine MongoDB Detected";
|
||||
public getTerminalSuppressedData(): string[] {
|
||||
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
|
||||
*/
|
||||
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(
|
||||
addSocketListener(this._socket, "message", (ev) => {
|
||||
let data: ArrayBuffer | string = ev.data;
|
||||
@@ -103,51 +115,136 @@ export class AttachAddon implements ITerminalAddon {
|
||||
data = enc.decode(ev.data as ArrayBuffer);
|
||||
}
|
||||
|
||||
// for example of json object look in TerminalHelper in the socket.onMessage
|
||||
if (data.includes(startStatusJson) && data.includes(endStatusJson)) {
|
||||
// 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 = "";
|
||||
}
|
||||
// Handle status messages
|
||||
let processedStatusData = data;
|
||||
|
||||
if (this._allowTerminalWrite && data.includes(this._startMarker)) {
|
||||
this._allowTerminalWrite = false;
|
||||
terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`);
|
||||
}
|
||||
|
||||
if (this._allowTerminalWrite) {
|
||||
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
|
||||
const hasSuppressedData = suppressedData && suppressedData.length > 0;
|
||||
|
||||
if (!hasSuppressedData || !data.includes(suppressedData)) {
|
||||
terminal.write(data);
|
||||
// Process status messages with delimiters
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const startIndex = processedStatusData.indexOf(startStatusJson);
|
||||
if (startIndex === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
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())) {
|
||||
this._allowTerminalWrite = true;
|
||||
// Add to message buffer
|
||||
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 {
|
||||
|
||||
@@ -50,3 +50,34 @@ export const getShellNameForDisplay = (terminalKind: TerminalKind): string => {
|
||||
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",
|
||||
"centralindia",
|
||||
"southeastasia",
|
||||
"westcentralus",
|
||||
"usgovvirginia",
|
||||
"usgovarizona",
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -39,7 +40,6 @@ export const getNormalizedRegion = (region: string, defaultCloudshellRegion: str
|
||||
}
|
||||
|
||||
const regionMap: Record<string, string> = {
|
||||
centralus: "westcentralus",
|
||||
eastus2: "eastus",
|
||||
};
|
||||
|
||||
|
||||
@@ -146,10 +146,16 @@ describe("Documents tab (Mongo API)", () => {
|
||||
updateConfigContext({ platform: Platform.Hosted });
|
||||
|
||||
const props: IDocumentsTabComponentProps = createMockProps();
|
||||
|
||||
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(() => {
|
||||
wrapper.unmount();
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface IGraphConfig {
|
||||
|
||||
interface GraphTabOptions extends ViewModels.TabOptions {
|
||||
account: DatabaseAccount;
|
||||
masterKey: string;
|
||||
password: string;
|
||||
collectionId: string;
|
||||
databaseId: string;
|
||||
collectionPartitionKeyProperty: string;
|
||||
@@ -107,7 +107,7 @@ export default class GraphTab extends TabsBase {
|
||||
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
|
||||
databaseId: options.databaseId,
|
||||
collectionId: options.collectionId,
|
||||
masterKey: options.masterKey,
|
||||
password: options.password,
|
||||
onLoadStartKey: options.onLoadStartKey,
|
||||
onLoadStartKeyChange: (onLoadStartKey: number): void => {
|
||||
if (onLoadStartKey === undefined) {
|
||||
|
||||
@@ -106,6 +106,6 @@ describe("QueryTabComponent", () => {
|
||||
<QueryTabCopilotComponent {...propsMock} />
|
||||
</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 { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
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-splitter-layout/lib/index.css";
|
||||
import { format } from "react-string-format";
|
||||
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
||||
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||
//TODO: Uncomment next two lines when query copilot is reinstated in DE
|
||||
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
||||
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
|
||||
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
@@ -494,53 +494,55 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
|
||||
const mainButtonLabel = "Launch Copilot";
|
||||
const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
|
||||
const copilotSettingLabel = "Copilot settings";
|
||||
//TODO: Uncomment next section when query copilot is reinstated in DE
|
||||
// if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
|
||||
// const mainButtonLabel = "Launch Copilot";
|
||||
// const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
|
||||
// const copilotSettingLabel = "Copilot settings";
|
||||
|
||||
const openCopilotChatButton: CommandButtonComponentProps = {
|
||||
iconAlt: chatPaneLabel,
|
||||
onCommandClick: this.launchQueryCopilotChat,
|
||||
commandButtonLabel: chatPaneLabel,
|
||||
ariaLabel: chatPaneLabel,
|
||||
hasPopup: false,
|
||||
};
|
||||
// const openCopilotChatButton: CommandButtonComponentProps = {
|
||||
// iconAlt: chatPaneLabel,
|
||||
// onCommandClick: this.launchQueryCopilotChat,
|
||||
// commandButtonLabel: chatPaneLabel,
|
||||
// ariaLabel: chatPaneLabel,
|
||||
// hasPopup: false,
|
||||
// };
|
||||
|
||||
const copilotSettingsButton: CommandButtonComponentProps = {
|
||||
iconAlt: copilotSettingLabel,
|
||||
onCommandClick: () => undefined,
|
||||
commandButtonLabel: copilotSettingLabel,
|
||||
ariaLabel: copilotSettingLabel,
|
||||
hasPopup: false,
|
||||
};
|
||||
// const copilotSettingsButton: CommandButtonComponentProps = {
|
||||
// iconAlt: copilotSettingLabel,
|
||||
// onCommandClick: () => undefined,
|
||||
// commandButtonLabel: copilotSettingLabel,
|
||||
// ariaLabel: copilotSettingLabel,
|
||||
// hasPopup: false,
|
||||
// };
|
||||
|
||||
const launchCopilotButton: CommandButtonComponentProps = {
|
||||
iconSrc: LaunchCopilot,
|
||||
iconAlt: mainButtonLabel,
|
||||
onCommandClick: this.launchQueryCopilotChat,
|
||||
commandButtonLabel: mainButtonLabel,
|
||||
ariaLabel: mainButtonLabel,
|
||||
hasPopup: false,
|
||||
children: [openCopilotChatButton, copilotSettingsButton],
|
||||
};
|
||||
buttons.push(launchCopilotButton);
|
||||
}
|
||||
// const launchCopilotButton: CommandButtonComponentProps = {
|
||||
// iconSrc: LaunchCopilot,
|
||||
// iconAlt: mainButtonLabel,
|
||||
// onCommandClick: this.launchQueryCopilotChat,
|
||||
// commandButtonLabel: mainButtonLabel,
|
||||
// ariaLabel: mainButtonLabel,
|
||||
// hasPopup: false,
|
||||
// children: [openCopilotChatButton, copilotSettingsButton],
|
||||
// };
|
||||
// buttons.push(launchCopilotButton);
|
||||
// }
|
||||
|
||||
if (this.props.copilotEnabled) {
|
||||
const toggleCopilotButton: CommandButtonComponentProps = {
|
||||
iconSrc: QueryCommandIcon,
|
||||
iconAlt: "Query Advisor",
|
||||
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
|
||||
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",
|
||||
hasPopup: false,
|
||||
};
|
||||
buttons.push(toggleCopilotButton);
|
||||
}
|
||||
//TODO: Uncomment next section when query copilot is reinstated in DE
|
||||
// if (this.props.copilotEnabled) {
|
||||
// const toggleCopilotButton: CommandButtonComponentProps = {
|
||||
// iconSrc: QueryCommandIcon,
|
||||
// iconAlt: "Query Advisor",
|
||||
// keyboardAction: KeyboardAction.TOGGLE_COPILOT,
|
||||
// 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",
|
||||
// hasPopup: false,
|
||||
// };
|
||||
// buttons.push(toggleCopilotButton);
|
||||
// }
|
||||
|
||||
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
|
||||
const label = "Cancel query";
|
||||
@@ -725,6 +727,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
return (
|
||||
<Fragment>
|
||||
<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 && (
|
||||
<QueryCopilotPromptbar
|
||||
explorer={this.props.collection.container}
|
||||
@@ -732,7 +735,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
databaseId={this.props.collection.databaseId}
|
||||
containerId={this.props.collection.id()}
|
||||
></QueryCopilotPromptbar>
|
||||
)}
|
||||
)} */}
|
||||
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
||||
<Allotment
|
||||
key={vertical.toString()}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFi
|
||||
import { getShellNameForDisplay } from "Explorer/Tabs/CloudShellTab/Utils/CommonUtils";
|
||||
import * as React from "react";
|
||||
import FirewallRuleScreenshot from "../../../../images/firewallRule.png";
|
||||
import VcoreFirewallRuleScreenshot from "../../../../images/vcoreMongoFirewallRule.png";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
@@ -25,15 +24,15 @@ export abstract class BaseTerminalComponentAdapter implements ReactAdapter {
|
||||
) {}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
if (this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo) {
|
||||
return this.renderTerminalComponent();
|
||||
}
|
||||
|
||||
if (!this.isAllPublicIPAddressesEnabled()) {
|
||||
return (
|
||||
<QuickstartFirewallNotification
|
||||
messageType={this.getMessageType()}
|
||||
screenshot={
|
||||
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
|
||||
? VcoreFirewallRuleScreenshot
|
||||
: FirewallRuleScreenshot
|
||||
}
|
||||
screenshot={FirewallRuleScreenshot}
|
||||
shellName={getShellNameForDisplay(this.kind)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -479,9 +480,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
node: this,
|
||||
title: title,
|
||||
tabPath: "",
|
||||
|
||||
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||
collection: this,
|
||||
masterKey: userContext.masterKey || "",
|
||||
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
||||
collectionId: this.id(),
|
||||
databaseId: this.databaseId,
|
||||
@@ -737,7 +737,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
title: title,
|
||||
tabPath: "",
|
||||
collection: this,
|
||||
masterKey: userContext.masterKey || "",
|
||||
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
||||
collectionId: this.id(),
|
||||
databaseId: this.databaseId,
|
||||
|
||||
@@ -8,6 +8,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
"children": [
|
||||
{
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Open Cassandra Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Table",
|
||||
@@ -23,6 +28,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
],
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Open Cassandra Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Table",
|
||||
@@ -45,6 +55,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
"children": [
|
||||
{
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Open Cassandra Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Table",
|
||||
@@ -65,6 +80,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
],
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Open Cassandra Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Table",
|
||||
@@ -123,6 +143,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
"children": [
|
||||
{
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Open Cassandra Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Table",
|
||||
@@ -138,6 +163,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
],
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Open Cassandra Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Table",
|
||||
@@ -187,6 +217,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
"children": [
|
||||
{
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Open Cassandra Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Table",
|
||||
@@ -257,6 +292,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
||||
],
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Open Cassandra Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Table",
|
||||
@@ -323,7 +363,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New Shell",
|
||||
"label": "Open Mongo Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
@@ -354,7 +394,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New Shell",
|
||||
"label": "Open Mongo Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
@@ -386,7 +426,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New Shell",
|
||||
"label": "Open Mongo Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
@@ -422,7 +462,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New Shell",
|
||||
"label": "Open Mongo Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
@@ -490,7 +530,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New Shell",
|
||||
"label": "Open Mongo Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
@@ -521,7 +561,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New Shell",
|
||||
"label": "Open Mongo Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
@@ -580,7 +620,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New Shell",
|
||||
"label": "Open Mongo Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
@@ -666,7 +706,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New Shell",
|
||||
"label": "Open Mongo Shell",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -34,7 +34,8 @@ const App: React.FunctionComponent = () => {
|
||||
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
||||
const config = useConfig();
|
||||
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
|
||||
useAADAuth();
|
||||
useAADAuth(config);
|
||||
|
||||
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
||||
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
||||
const [connectionString, setConnectionString] = React.useState<string>();
|
||||
|
||||
@@ -252,7 +252,7 @@ export class PhoenixClient {
|
||||
|
||||
private getPhoenixControlPlanePathPrefix(): string {
|
||||
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 =
|
||||
|
||||
@@ -4,6 +4,10 @@ import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||
import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
|
||||
import { FabricArtifactInfo, updateUserContext, userContext } from "UserContext";
|
||||
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 DEBOUNCE_DELAY_MS = 1000 * 20; // 20 second
|
||||
|
||||
@@ -111,7 +111,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||
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,
|
||||
path: getOfferingIdPathForRegion(),
|
||||
method: "GET",
|
||||
apiVersion: "2023-05-01-preview",
|
||||
apiVersion: configContext.CATALOG_API_VERSION,
|
||||
queryParams: {
|
||||
filter:
|
||||
"armRegionName eq '" +
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MongoGuidRepresentation } from "Common/Constants";
|
||||
import { SplitterDirection } from "Common/Splitter";
|
||||
import * as LocalStorageUtility from "./LocalStorageUtility";
|
||||
import * as SessionStorageUtility from "./SessionStorageUtility";
|
||||
@@ -33,6 +34,8 @@ export enum StorageKey {
|
||||
DocumentsTabPrefs,
|
||||
DefaultQueryResultsView,
|
||||
AppState,
|
||||
MongoGuidRepresentation,
|
||||
IgnorePartitionKeyOnDocumentUpdate,
|
||||
}
|
||||
|
||||
export const hasRUThresholdBeenConfigured = (): boolean => {
|
||||
@@ -65,4 +68,13 @@ export const getDefaultQueryResultsView = (): SplitterDirection => {
|
||||
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;
|
||||
|
||||
@@ -150,6 +150,7 @@ export enum Action {
|
||||
CloudShellUserConsent,
|
||||
CloudShellTerminalSession,
|
||||
OpenVSCode,
|
||||
ImportSampleData,
|
||||
}
|
||||
|
||||
export const ActionModifiers = {
|
||||
|
||||
@@ -91,5 +91,11 @@ export const getItemName = (): string => {
|
||||
};
|
||||
|
||||
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 * as Constants from "../Common/Constants";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { ApiType, updateUserContext, userContext } from "../UserContext";
|
||||
import * as AuthorizationUtils from "./AuthorizationUtils";
|
||||
jest.mock("../Explorer/Explorer");
|
||||
|
||||
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()", () => {
|
||||
it("should return authorization header if authentication type is AAD", () => {
|
||||
updateUserContext({
|
||||
@@ -54,4 +95,41 @@ describe("AuthorizationUtils", () => {
|
||||
).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 { getEnvironmentScopeEndpoint } from "Common/EnvironmentUtility";
|
||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { hasProxyServer, isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import { AuthType } from "../AuthType";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
@@ -7,7 +9,7 @@ import { configContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { UserContext, userContext } from "../UserContext";
|
||||
|
||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
@@ -73,10 +75,12 @@ export async function acquireMsalTokenForAccount(
|
||||
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
|
||||
throw new Error("Database account has no document endpoint defined");
|
||||
}
|
||||
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
||||
/\/+$/,
|
||||
"/.default",
|
||||
);
|
||||
let hrefEndpoint = "";
|
||||
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||
hrefEndpoint = getEnvironmentScopeEndpoint();
|
||||
} else {
|
||||
hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(/\/+$/, "/.default");
|
||||
}
|
||||
const msalInstance = await getMsalInstance();
|
||||
const knownAccounts = msalInstance.getAllAccounts();
|
||||
// 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 autoPilotIncrementStep = 1000;
|
||||
export const autoPilotThroughput4K = 4000;
|
||||
export const autoPilotThroughput5K = 5000;
|
||||
export const autoPilotThroughput10K = 10000;
|
||||
|
||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
@@ -18,5 +19,8 @@ export const isServerlessAccount = (): 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",
|
||||
];
|
||||
|
||||
export const allowedAadEndpoints: ReadonlyArray<string> = [
|
||||
export const defaultAllowedAadEndpoints: ReadonlyArray<string> = [
|
||||
"https://login.microsoftonline.com/",
|
||||
"https://login.microsoftonline.us/",
|
||||
"https://login.partner.microsoftonline.cn/",
|
||||
];
|
||||
|
||||
export const defaultAllowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.microsoft.com"];
|
||||
|
||||
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
||||
"https://localhost:12901",
|
||||
"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> = [
|
||||
"https://localhost:1234",
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
@@ -86,19 +79,8 @@ export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
||||
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 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 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