mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 10:51:30 +00:00
Compare commits
21 Commits
unit-tests
...
cloudshell
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
811a6dd363 | ||
|
|
012d043c78 | ||
|
|
3afd74a957 | ||
|
|
0ef4399ba4 | ||
|
|
870863a723 | ||
|
|
e3815734db | ||
|
|
5ea78f9abf | ||
|
|
8a56214ec2 | ||
|
|
e3ae006100 | ||
|
|
589b61afaf | ||
|
|
eb3f6bc93f | ||
|
|
6ec909a97b | ||
|
|
08a51ca6b1 | ||
|
|
30a3b5c7a4 | ||
|
|
f370507a27 | ||
|
|
e0edaf405c | ||
|
|
f8231600d6 | ||
|
|
45c8d70c77 | ||
|
|
70d7ee755b | ||
|
|
0a4aed4f47 | ||
|
|
a7d007e0dd |
@@ -23,8 +23,6 @@ src/Common/MongoUtility.ts
|
|||||||
src/Common/NotificationsClientBase.ts
|
src/Common/NotificationsClientBase.ts
|
||||||
src/Common/QueriesClient.ts
|
src/Common/QueriesClient.ts
|
||||||
src/Common/Splitter.ts
|
src/Common/Splitter.ts
|
||||||
src/Controls/Heatmap/Heatmap.test.ts
|
|
||||||
src/Controls/Heatmap/Heatmap.ts
|
|
||||||
src/Definitions/datatables.d.ts
|
src/Definitions/datatables.d.ts
|
||||||
src/Definitions/gif.d.ts
|
src/Definitions/gif.d.ts
|
||||||
src/Definitions/globals.d.ts
|
src/Definitions/globals.d.ts
|
||||||
|
|||||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -177,9 +177,27 @@ jobs:
|
|||||||
- name: "Az CLI login"
|
- name: "Az CLI login"
|
||||||
uses: Azure/login@v2
|
uses: Azure/login@v2
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
|
||||||
|
- name: "Acquire RBAC tokens for test accounts"
|
||||||
|
uses: azure/cli@v2
|
||||||
|
with:
|
||||||
|
azcliversion: latest
|
||||||
|
inlineScript: |
|
||||||
|
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
|
||||||
|
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
||||||
|
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
||||||
|
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
|
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
|
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
|
||||||
|
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
- name: Upload blob report to GitHub Actions Artifacts
|
||||||
|
|||||||
2
.github/workflows/cleanup.yml
vendored
2
.github/workflows/cleanup.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: "Az CLI login"
|
- name: "Az CLI login"
|
||||||
uses: azure/login@v1
|
uses: azure/login@v1
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||||
"isTerminalEnabled": true,
|
|
||||||
"isPhoenixEnabled": true
|
"isPhoenixEnabled": true
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||||
"isTerminalEnabled" : false,
|
"isPhoenixEnabled": false
|
||||||
"isPhoenixEnabled" : false
|
|
||||||
}
|
}
|
||||||
37
package-lock.json
generated
37
package-lock.json
generated
@@ -10,7 +10,7 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "4.3.0",
|
"@azure/cosmos": "4.5.0",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "4.5.0",
|
"@azure/identity": "4.5.0",
|
||||||
"@azure/msal-browser": "2.14.2",
|
"@azure/msal-browser": "2.14.2",
|
||||||
@@ -391,24 +391,25 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@azure/cosmos": {
|
"node_modules/@azure/cosmos": {
|
||||||
"version": "4.3.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.5.0.tgz",
|
||||||
"integrity": "sha512-0Ls3l1uWBBSphx6YRhnM+w7rSvq8qVugBCdO6kSiNuRYXEf6+YWLjbzz4e7L2kkz/6ScFdZIOJYP+XtkiRYOhA==",
|
"integrity": "sha512-JsTh4twb6FcwP7rJwxQiNZQ/LGtuF6gmciaxY9Rnp6/A325Lhsw/SH4R2ArpT0yCvozbZpweIwdPfUkXVBtp5w==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/abort-controller": "^2.0.0",
|
"@azure/abort-controller": "^2.1.2",
|
||||||
"@azure/core-auth": "^1.7.1",
|
"@azure/core-auth": "^1.9.0",
|
||||||
"@azure/core-rest-pipeline": "^1.15.1",
|
"@azure/core-rest-pipeline": "^1.19.1",
|
||||||
"@azure/core-tracing": "^1.1.1",
|
"@azure/core-tracing": "^1.2.0",
|
||||||
"@azure/core-util": "^1.8.1",
|
"@azure/core-util": "^1.11.0",
|
||||||
"@azure/keyvault-keys": "^4.8.0",
|
"@azure/keyvault-keys": "^4.9.0",
|
||||||
|
"@azure/logger": "^1.1.4",
|
||||||
"fast-json-stable-stringify": "^2.1.0",
|
"fast-json-stable-stringify": "^2.1.0",
|
||||||
"jsbi": "^4.3.0",
|
|
||||||
"priorityqueuejs": "^2.0.0",
|
"priorityqueuejs": "^2.0.0",
|
||||||
"semaphore": "^1.1.0",
|
"semaphore": "^1.1.0",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/cosmos-language-service": {
|
"node_modules/@azure/cosmos-language-service": {
|
||||||
@@ -438,8 +439,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/cosmos/node_modules/tslib": {
|
"node_modules/@azure/cosmos/node_modules/tslib": {
|
||||||
"version": "2.6.2",
|
"version": "2.8.1",
|
||||||
"license": "0BSD"
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
},
|
},
|
||||||
"node_modules/@azure/identity": {
|
"node_modules/@azure/identity": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
@@ -27178,11 +27180,6 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsbi": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g=="
|
|
||||||
},
|
|
||||||
"node_modules/jsbn": {
|
"node_modules/jsbn": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "4.3.0",
|
"@azure/cosmos": "4.5.0",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "4.5.0",
|
"@azure/identity": "4.5.0",
|
||||||
"@azure/msal-browser": "2.14.2",
|
"@azure/msal-browser": "2.14.2",
|
||||||
|
|||||||
@@ -138,15 +138,6 @@ export enum MongoBackendEndpointType {
|
|||||||
remote,
|
remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BackendApi {
|
|
||||||
public static readonly GenerateToken: string = "GenerateToken";
|
|
||||||
public static readonly PortalSettings: string = "PortalSettings";
|
|
||||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
|
||||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
|
||||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
|
||||||
public static readonly SampleData: string = "SampleData";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
export class PortalBackendEndpoints {
|
||||||
public static readonly Development: string = "https://localhost:7235";
|
public static readonly Development: string = "https://localhost:7235";
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
|||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
import { PriorityLevel } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
||||||
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
@@ -20,8 +20,7 @@ const _global = typeof self === "undefined" ? window : self;
|
|||||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
|
|
||||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
if (useDataplaneRbacAuthorization(userContext)) {
|
||||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||||
"Explorer/tokenProvider",
|
"Explorer/tokenProvider",
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -84,7 +83,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
queryDocuments(databaseId, collection, true, "{}");
|
queryDocuments(databaseId, collection, true, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -101,7 +99,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -120,7 +117,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -137,7 +133,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -156,7 +151,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -173,7 +167,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -197,7 +190,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -216,7 +208,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
deleteDocuments(databaseId, collection, [documentId]);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -233,7 +224,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
||||||
{
|
{
|
||||||
"disableNonStreamingOrderByQuery": true,
|
|
||||||
"enableQueryControl": false,
|
"enableQueryControl": false,
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
@@ -14,7 +13,6 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] =
|
|||||||
|
|
||||||
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
||||||
{
|
{
|
||||||
"disableNonStreamingOrderByQuery": true,
|
|
||||||
"enableQueryControl": false,
|
"enableQueryControl": false,
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -41,7 +42,7 @@ interface MetricsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||||
if (userContext.authType !== AuthType.AAD) {
|
if (userContext.authType !== AuthType.AAD || isFabricNative()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
|
||||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
import { Queries } from "../Constants";
|
import { Queries } from "../Constants";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
@@ -28,6 +27,5 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
|||||||
Queries.itemsPerPage;
|
Queries.itemsPerPage;
|
||||||
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
|
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
|
||||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||||
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
|
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
|
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||||
import {
|
import {
|
||||||
BackendApi,
|
|
||||||
CassandraProxyEndpoints,
|
|
||||||
JunoEndpoints,
|
|
||||||
MongoProxyEndpoints,
|
|
||||||
PortalBackendEndpoints,
|
|
||||||
} from "Common/Constants";
|
|
||||||
import {
|
|
||||||
allowedAadEndpoints,
|
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
|
defaultAllowedAadEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
defaultAllowedCassandraProxyEndpoints,
|
defaultAllowedCassandraProxyEndpoints,
|
||||||
|
defaultAllowedGraphEndpoints,
|
||||||
defaultAllowedMongoProxyEndpoints,
|
defaultAllowedMongoProxyEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
@@ -29,6 +23,8 @@ export enum Platform {
|
|||||||
|
|
||||||
export interface ConfigContext {
|
export interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
|
allowedAadEndpoints: ReadonlyArray<string>;
|
||||||
|
allowedGraphEndpoints: ReadonlyArray<string>;
|
||||||
allowedArmEndpoints: ReadonlyArray<string>;
|
allowedArmEndpoints: ReadonlyArray<string>;
|
||||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||||
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||||
@@ -37,10 +33,8 @@ export interface ConfigContext {
|
|||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
proxyPath?: string;
|
proxyPath?: string;
|
||||||
AAD_ENDPOINT: string;
|
AAD_ENDPOINT: string;
|
||||||
ARM_AUTH_AREA: string;
|
|
||||||
ARM_ENDPOINT: string;
|
ARM_ENDPOINT: string;
|
||||||
EMULATOR_ENDPOINT?: string;
|
EMULATOR_ENDPOINT?: string;
|
||||||
ARM_API_VERSION: string;
|
|
||||||
GRAPH_ENDPOINT: string;
|
GRAPH_ENDPOINT: string;
|
||||||
GRAPH_API_VERSION: string;
|
GRAPH_API_VERSION: string;
|
||||||
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
|
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
|
||||||
@@ -50,27 +44,24 @@ export interface ConfigContext {
|
|||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
PORTAL_BACKEND_ENDPOINT: string;
|
PORTAL_BACKEND_ENDPOINT: string;
|
||||||
NEW_BACKEND_APIS?: BackendApi[];
|
|
||||||
MONGO_PROXY_ENDPOINT: string;
|
MONGO_PROXY_ENDPOINT: string;
|
||||||
CASSANDRA_PROXY_ENDPOINT: string;
|
CASSANDRA_PROXY_ENDPOINT: string;
|
||||||
NEW_CASSANDRA_APIS?: string[];
|
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
GITHUB_CLIENT_ID: string;
|
GITHUB_CLIENT_ID: string;
|
||||||
GITHUB_TEST_ENV_CLIENT_ID: string;
|
GITHUB_TEST_ENV_CLIENT_ID: string;
|
||||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||||
isTerminalEnabled: boolean;
|
|
||||||
isPhoenixEnabled: boolean;
|
isPhoenixEnabled: boolean;
|
||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
msalRedirectURI?: string;
|
msalRedirectURI?: string;
|
||||||
globallyEnabledCassandraAPIs?: string[];
|
|
||||||
globallyEnabledMongoAPIs?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
let configContext: Readonly<ConfigContext> = {
|
let configContext: Readonly<ConfigContext> = {
|
||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
|
allowedAadEndpoints: defaultAllowedAadEndpoints,
|
||||||
|
allowedGraphEndpoints: defaultAllowedGraphEndpoints,
|
||||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||||
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||||
@@ -85,17 +76,12 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
||||||
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
||||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
|
||||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
|
||||||
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||||
ARM_AUTH_AREA: "https://management.azure.com/",
|
|
||||||
ARM_ENDPOINT: "https://management.azure.com/",
|
ARM_ENDPOINT: "https://management.azure.com/",
|
||||||
ARM_API_VERSION: "2016-06-01",
|
|
||||||
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
||||||
GRAPH_API_VERSION: "1.6",
|
GRAPH_API_VERSION: "1.6",
|
||||||
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
||||||
@@ -109,11 +95,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
|
||||||
isTerminalEnabled: false,
|
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
globallyEnabledCassandraAPIs: [],
|
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resetConfigContext(): void {
|
export function resetConfigContext(): void {
|
||||||
@@ -128,19 +110,21 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
if (!validateEndpoint(newContext.AAD_ENDPOINT, configContext.allowedAadEndpoints || defaultAllowedAadEndpoints)) {
|
||||||
delete newContext.ARM_ENDPOINT;
|
delete newContext.AAD_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
|
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
||||||
delete newContext.AAD_ENDPOINT;
|
delete newContext.ARM_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
||||||
delete newContext.EMULATOR_ENDPOINT;
|
delete newContext.EMULATOR_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
|
if (
|
||||||
|
!validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints || defaultAllowedGraphEndpoints)
|
||||||
|
) {
|
||||||
delete newContext.GRAPH_ENDPOINT;
|
delete newContext.GRAPH_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +132,15 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.ARCADIA_ENDPOINT;
|
delete newContext.ARCADIA_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!validateEndpoint(
|
||||||
|
newContext.PORTAL_BACKEND_ENDPOINT,
|
||||||
|
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete newContext.PORTAL_BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!validateEndpoint(
|
!validateEndpoint(
|
||||||
newContext.MONGO_PROXY_ENDPOINT,
|
newContext.MONGO_PROXY_ENDPOINT,
|
||||||
|
|||||||
@@ -443,6 +443,7 @@ export interface DataExplorerInputsFrame {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
feedbackPolicies?: any;
|
feedbackPolicies?: any;
|
||||||
|
aadToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelfServeFrameInputs {
|
export interface SelfServeFrameInputs {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html class="no-js" lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="data:," />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="heatmap"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
@import "../../../less/Common/Constants";
|
|
||||||
html {
|
|
||||||
font-family: @DataExplorerFont;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
border: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: @DataExplorerFont;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
border: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#heatmap {
|
|
||||||
.dark-theme {
|
|
||||||
color: @BaseLight;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chartTitle {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
left: 3px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noDataMessage {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10000;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
opacity: 0.97;
|
|
||||||
div {
|
|
||||||
border-color: rgba(204, 204, 204, 0.8);
|
|
||||||
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.12);
|
|
||||||
padding: 15px 10px;
|
|
||||||
width: calc(55% - 40px);
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: center;
|
|
||||||
border-width: 1px;
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { handleMessage, Heatmap, isDarkTheme } from "./Heatmap";
|
|
||||||
import { PortalTheme } from "./HeatmapDatatypes";
|
|
||||||
|
|
||||||
describe("The Heatmap Control", () => {
|
|
||||||
const dataPoints = {
|
|
||||||
"1": {
|
|
||||||
"2019-06-19T00:59:10Z": {
|
|
||||||
"Normalized Throughput": 0.35,
|
|
||||||
},
|
|
||||||
"2019-06-19T00:48:10Z": {
|
|
||||||
"Normalized Throughput": 0.25,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartCaptions = {
|
|
||||||
chartTitle: "chart title",
|
|
||||||
yAxisTitle: "YAxisTitle",
|
|
||||||
tooltipText: "Tooltip text",
|
|
||||||
timeWindow: 123456789,
|
|
||||||
};
|
|
||||||
|
|
||||||
let heatmap: Heatmap;
|
|
||||||
const theme: PortalTheme = 1;
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
|
||||||
|
|
||||||
describe("drawHeatmap rendering", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
heatmap = new Heatmap(dataPoints, chartCaptions, theme);
|
|
||||||
document.body.innerHTML = divElement;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
document.body.innerHTML = ``;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call _getChartSettings when drawHeatmap is invoked", () => {
|
|
||||||
const _getChartSettings = jest.spyOn(heatmap, "_getChartSettings");
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(_getChartSettings).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call _getLayoutSettings when drawHeatmap is invoked", () => {
|
|
||||||
const _getLayoutSettings = jest.spyOn(heatmap, "_getLayoutSettings");
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(_getLayoutSettings).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call _getChartDisplaySettings when drawHeatmap is invoked", () => {
|
|
||||||
const _getChartDisplaySettings = jest.spyOn(heatmap, "_getChartDisplaySettings");
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(_getChartDisplaySettings).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("drawHeatmap should render a Heatmap inside the div element", () => {
|
|
||||||
heatmap.drawHeatmap();
|
|
||||||
expect(document.body.innerHTML).not.toEqual(divElement);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("generateMatrixFromMap", () => {
|
|
||||||
it("should massage input data to match output expected", () => {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).yAxisPoints).toEqual(["1"]);
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).dataPoints).toEqual([[0.25, 0.35]]);
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints.length).toEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should output the date format to ISO8601 string format", () => {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(10, 11)).toEqual("T");
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(-1)).toEqual("Z");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should convert the time to the user's local time", () => {
|
|
||||||
if (dayjs().utcOffset()) {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([
|
|
||||||
"2019-06-19T00:48:10Z",
|
|
||||||
"2019-06-19T00:59:10Z",
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([
|
|
||||||
"2019-06-19T00:48:10Z",
|
|
||||||
"2019-06-19T00:59:10Z",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isDarkTheme", () => {
|
|
||||||
it("isDarkTheme should return the correct result", () => {
|
|
||||||
expect(isDarkTheme(PortalTheme.dark)).toEqual(true);
|
|
||||||
expect(isDarkTheme(PortalTheme.azure)).not.toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("iframe rendering when there is no data", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
document.body.innerHTML = ``;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show a no data message with a dark theme", () => {
|
|
||||||
const data = {
|
|
||||||
data: {
|
|
||||||
signature: "pcIframe",
|
|
||||||
data: {
|
|
||||||
chartData: {},
|
|
||||||
chartSettings: {},
|
|
||||||
theme: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
origin: "http://localhost",
|
|
||||||
};
|
|
||||||
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
|
||||||
document.body.innerHTML = divElement;
|
|
||||||
|
|
||||||
handleMessage(data as MessageEvent);
|
|
||||||
expect(document.body.innerHTML).toContain("dark-theme");
|
|
||||||
expect(document.body.innerHTML).toContain("noDataMessage");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show a no data message with a white theme", () => {
|
|
||||||
const data = {
|
|
||||||
data: {
|
|
||||||
signature: "pcIframe",
|
|
||||||
data: {
|
|
||||||
chartData: {},
|
|
||||||
chartSettings: {},
|
|
||||||
theme: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
origin: "http://localhost",
|
|
||||||
};
|
|
||||||
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
|
||||||
document.body.innerHTML = divElement;
|
|
||||||
|
|
||||||
handleMessage(data as MessageEvent);
|
|
||||||
expect(document.body.innerHTML).not.toContain("dark-theme");
|
|
||||||
expect(document.body.innerHTML).toContain("noDataMessage");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import * as Plotly from "plotly.js-cartesian-dist-min";
|
|
||||||
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
|
|
||||||
import { StyleConstants } from "../../Common/StyleConstants";
|
|
||||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
|
||||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
|
||||||
import "./Heatmap.less";
|
|
||||||
import {
|
|
||||||
ChartSettings,
|
|
||||||
DataPayload,
|
|
||||||
DisplaySettings,
|
|
||||||
FontSettings,
|
|
||||||
HeatmapCaptions,
|
|
||||||
HeatmapData,
|
|
||||||
LayoutSettings,
|
|
||||||
PartitionTimeStampToData,
|
|
||||||
PortalTheme,
|
|
||||||
} from "./HeatmapDatatypes";
|
|
||||||
|
|
||||||
export class Heatmap {
|
|
||||||
public static readonly elementId: string = "heatmap";
|
|
||||||
|
|
||||||
private _chartData: HeatmapData;
|
|
||||||
private _heatmapCaptions: HeatmapCaptions;
|
|
||||||
private _theme: PortalTheme;
|
|
||||||
private _defaultFontColor: string;
|
|
||||||
|
|
||||||
constructor(data: DataPayload, heatmapCaptions: HeatmapCaptions, theme: PortalTheme) {
|
|
||||||
this._theme = theme;
|
|
||||||
this._defaultFontColor = StyleConstants.BaseDark;
|
|
||||||
this._setThemeColorForChart();
|
|
||||||
this._chartData = this.generateMatrixFromMap(data);
|
|
||||||
this._heatmapCaptions = heatmapCaptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setThemeColorForChart() {
|
|
||||||
if (isDarkTheme(this._theme)) {
|
|
||||||
this._defaultFontColor = StyleConstants.BaseLight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings {
|
|
||||||
return {
|
|
||||||
family: StyleConstants.DataExplorerFont,
|
|
||||||
size,
|
|
||||||
color,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public generateMatrixFromMap(data: DataPayload): HeatmapData {
|
|
||||||
// all keys in data payload, sorted...
|
|
||||||
const rows: string[] = Object.keys(data).sort((a: string, b: string) => {
|
|
||||||
if (parseInt(a) < parseInt(b)) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
if (parseInt(a) > parseInt(b)) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const output: HeatmapData = {
|
|
||||||
yAxisPoints: [],
|
|
||||||
dataPoints: [],
|
|
||||||
xAxisPoints: Object.keys(data[rows[0]]).sort((a: string, b: string) => {
|
|
||||||
if (a < b) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
if (a > b) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
// go thru all rows and create 2d matrix for heatmap...
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
|
||||||
output.yAxisPoints.push(rows[i]);
|
|
||||||
const dataPoints: number[] = [];
|
|
||||||
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
|
||||||
const row: PartitionTimeStampToData = data[rows[i]];
|
|
||||||
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
|
|
||||||
}
|
|
||||||
output.dataPoints.push(dataPoints);
|
|
||||||
}
|
|
||||||
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
|
||||||
const dateTime = output.xAxisPoints[a];
|
|
||||||
// convert to local users timezone...
|
|
||||||
const day = dayjs(new Date(dateTime)).format("YYYY-MM-DD");
|
|
||||||
const hour = dayjs(new Date(dateTime)).format("HH:mm:ss");
|
|
||||||
// coerce to ISOString format since that is what plotly wants...
|
|
||||||
output.xAxisPoints[a] = `${day}T${hour}Z`;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
// public for testing purposes
|
|
||||||
public _getChartSettings(): ChartSettings[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
z: this._chartData.dataPoints,
|
|
||||||
type: "heatmap",
|
|
||||||
zmin: 0,
|
|
||||||
zmid: 50,
|
|
||||||
zmax: 100,
|
|
||||||
colorscale: [
|
|
||||||
[0.0, "#1FD338"],
|
|
||||||
[0.1, "#1CAD2F"],
|
|
||||||
[0.2, "#50A527"],
|
|
||||||
[0.3, "#719F21"],
|
|
||||||
[0.4, "#95991B"],
|
|
||||||
[0.5, "#CE8F11"],
|
|
||||||
[0.6, "#E27F0F"],
|
|
||||||
[0.7, "#E46612"],
|
|
||||||
[0.8, "#E64914"],
|
|
||||||
[0.9, "#B80016"],
|
|
||||||
[1.0, "#B80016"],
|
|
||||||
],
|
|
||||||
name: "",
|
|
||||||
hovertemplate: this._heatmapCaptions.tooltipText,
|
|
||||||
colorbar: {
|
|
||||||
thickness: 15,
|
|
||||||
outlinewidth: 0,
|
|
||||||
tickcolor: StyleConstants.BaseDark,
|
|
||||||
tickfont: this._getFontStyles(10, this._defaultFontColor),
|
|
||||||
},
|
|
||||||
y: this._chartData.yAxisPoints,
|
|
||||||
x: this._chartData.xAxisPoints,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// public for testing purposes
|
|
||||||
public _getLayoutSettings(): LayoutSettings {
|
|
||||||
return {
|
|
||||||
margin: {
|
|
||||||
l: 40,
|
|
||||||
r: 10,
|
|
||||||
b: 35,
|
|
||||||
t: 30,
|
|
||||||
pad: 0,
|
|
||||||
},
|
|
||||||
paper_bgcolor: "transparent",
|
|
||||||
plot_bgcolor: "transparent",
|
|
||||||
width: 462,
|
|
||||||
height: 240,
|
|
||||||
yaxis: {
|
|
||||||
title: this._heatmapCaptions.yAxisTitle,
|
|
||||||
titlefont: this._getFontStyles(11),
|
|
||||||
autorange: true,
|
|
||||||
showgrid: false,
|
|
||||||
zeroline: false,
|
|
||||||
showline: false,
|
|
||||||
autotick: true,
|
|
||||||
fixedrange: true,
|
|
||||||
ticks: "",
|
|
||||||
showticklabels: false,
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
fixedrange: true,
|
|
||||||
title: "*White area in heatmap indicates there is no available data",
|
|
||||||
titlefont: this._getFontStyles(11),
|
|
||||||
autorange: true,
|
|
||||||
showgrid: false,
|
|
||||||
zeroline: false,
|
|
||||||
showline: false,
|
|
||||||
autotick: true,
|
|
||||||
tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e",
|
|
||||||
showticklabels: true,
|
|
||||||
tickfont: this._getFontStyles(10),
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: this._heatmapCaptions.chartTitle,
|
|
||||||
x: 0.01,
|
|
||||||
font: this._getFontStyles(13, this._defaultFontColor),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// public for testing purposes
|
|
||||||
public _getChartDisplaySettings(): DisplaySettings {
|
|
||||||
return {
|
|
||||||
/* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings
|
|
||||||
responsive: true,*/
|
|
||||||
displayModeBar: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public drawHeatmap(): void {
|
|
||||||
// todo - create random elementId generator so multiple heatmaps can be created - ticket # 431469
|
|
||||||
Plotly.plot(
|
|
||||||
Heatmap.elementId,
|
|
||||||
this._getChartSettings(),
|
|
||||||
this._getLayoutSettings(),
|
|
||||||
this._getChartDisplaySettings(),
|
|
||||||
);
|
|
||||||
const plotDiv: any = document.getElementById(Heatmap.elementId);
|
|
||||||
plotDiv.on("plotly_click", (data: any) => {
|
|
||||||
let timeSelected: string = data.points[0].x;
|
|
||||||
timeSelected = timeSelected.replace(" ", "T");
|
|
||||||
timeSelected = `${timeSelected}Z`;
|
|
||||||
let xAxisIndex = 0;
|
|
||||||
for (let i = 0; i < this._chartData.xAxisPoints.length; i++) {
|
|
||||||
if (this._chartData.xAxisPoints[i] === timeSelected) {
|
|
||||||
xAxisIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const output = [];
|
|
||||||
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
|
|
||||||
output.push(this._chartData.dataPoints[i][xAxisIndex]);
|
|
||||||
}
|
|
||||||
sendCachedDataMessage(MessageTypes.LogInfo, output);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDarkTheme(theme: PortalTheme) {
|
|
||||||
return theme === PortalTheme.dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleMessage(event: MessageEvent) {
|
|
||||||
if (isInvalidParentFrameOrigin(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof event.data.data !== "object" ||
|
|
||||||
!("chartData" in event.data.data) ||
|
|
||||||
!("chartSettings" in event.data.data)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Plotly.purge(Heatmap.elementId);
|
|
||||||
|
|
||||||
document.getElementById(Heatmap.elementId)!.innerHTML = "";
|
|
||||||
const data = event.data.data;
|
|
||||||
const chartData: DataPayload = data.chartData;
|
|
||||||
const chartSettings: HeatmapCaptions = data.chartSettings;
|
|
||||||
const chartTheme: PortalTheme = data.theme;
|
|
||||||
if (Object.keys(chartData).length) {
|
|
||||||
new Heatmap(chartData, chartSettings, chartTheme).drawHeatmap();
|
|
||||||
} else {
|
|
||||||
const chartTitleElement = document.createElement("div");
|
|
||||||
chartTitleElement.innerHTML = data.chartSettings.chartTitle;
|
|
||||||
chartTitleElement.classList.add("chartTitle");
|
|
||||||
|
|
||||||
const noDataMessageElement = document.createElement("div");
|
|
||||||
noDataMessageElement.classList.add("noDataMessage");
|
|
||||||
const noDataMessageContent = document.createElement("div");
|
|
||||||
noDataMessageContent.innerHTML = data.errorMessage;
|
|
||||||
|
|
||||||
noDataMessageElement.appendChild(noDataMessageContent);
|
|
||||||
|
|
||||||
if (isDarkTheme(chartTheme)) {
|
|
||||||
chartTitleElement.classList.add("dark-theme");
|
|
||||||
noDataMessageElement.classList.add("dark-theme");
|
|
||||||
noDataMessageContent.classList.add("dark-theme");
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement);
|
|
||||||
document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("message", handleMessage, false);
|
|
||||||
sendReadyMessage();
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
type dataPoint = string | number;
|
|
||||||
|
|
||||||
export interface DataPayload {
|
|
||||||
[id: string]: PartitionTimeStampToData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PortalTheme {
|
|
||||||
blue = 1,
|
|
||||||
azure,
|
|
||||||
light,
|
|
||||||
dark,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HeatmapData {
|
|
||||||
yAxisPoints: string[];
|
|
||||||
xAxisPoints: string[];
|
|
||||||
dataPoints: dataPoint[][];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HeatmapCaptions {
|
|
||||||
chartTitle: string;
|
|
||||||
yAxisTitle: string;
|
|
||||||
tooltipText: string;
|
|
||||||
timeWindow: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FontSettings {
|
|
||||||
family: string;
|
|
||||||
size: number;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LayoutSettings {
|
|
||||||
paper_bgcolor?: string;
|
|
||||||
plot_bgcolor?: string;
|
|
||||||
margin?: {
|
|
||||||
l: number;
|
|
||||||
r: number;
|
|
||||||
b: number;
|
|
||||||
t: number;
|
|
||||||
pad: number;
|
|
||||||
};
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
yaxis?: {
|
|
||||||
fixedrange: boolean;
|
|
||||||
title: HeatmapCaptions["yAxisTitle"];
|
|
||||||
titlefont: FontSettings;
|
|
||||||
autorange: boolean;
|
|
||||||
showgrid: boolean;
|
|
||||||
zeroline: boolean;
|
|
||||||
showline: boolean;
|
|
||||||
autotick: boolean;
|
|
||||||
ticks: "";
|
|
||||||
showticklabels: boolean;
|
|
||||||
};
|
|
||||||
xaxis?: {
|
|
||||||
fixedrange: boolean;
|
|
||||||
title: string;
|
|
||||||
titlefont: FontSettings;
|
|
||||||
autorange: boolean;
|
|
||||||
showgrid: boolean;
|
|
||||||
zeroline: boolean;
|
|
||||||
showline: boolean;
|
|
||||||
autotick: boolean;
|
|
||||||
showticklabels: boolean;
|
|
||||||
tickformat: string;
|
|
||||||
tickfont: FontSettings;
|
|
||||||
};
|
|
||||||
title?: {
|
|
||||||
text: HeatmapCaptions["chartTitle"];
|
|
||||||
x: number;
|
|
||||||
font?: FontSettings;
|
|
||||||
};
|
|
||||||
font?: FontSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChartSettings {
|
|
||||||
z: HeatmapData["dataPoints"];
|
|
||||||
type: "heatmap";
|
|
||||||
zmin: number;
|
|
||||||
zmid: number;
|
|
||||||
zmax: number;
|
|
||||||
colorscale: [number, string][];
|
|
||||||
name: string;
|
|
||||||
hovertemplate: HeatmapCaptions["tooltipText"];
|
|
||||||
colorbar: {
|
|
||||||
thickness: number;
|
|
||||||
outlinewidth: number;
|
|
||||||
tickcolor: string;
|
|
||||||
tickfont: FontSettings;
|
|
||||||
};
|
|
||||||
y: HeatmapData["yAxisPoints"];
|
|
||||||
x: HeatmapData["xAxisPoints"];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DisplaySettings {
|
|
||||||
displayModeBar: boolean;
|
|
||||||
responsive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PartitionTimeStampToData {
|
|
||||||
[timeSeriesDates: string]: {
|
|
||||||
[NormalizedThroughput: string]: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -559,26 +559,81 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
private getThroughputTextField = (): JSX.Element => (
|
private getThroughputTextField = (): JSX.Element => (
|
||||||
<>
|
<>
|
||||||
{this.props.isAutoPilotSelected ? (
|
{this.props.isAutoPilotSelected ? (
|
||||||
<TextField
|
<Stack horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
|
||||||
label="Maximum RU/s required by this resource"
|
{/* Column 1: Minimum RU/s */}
|
||||||
required
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
type="number"
|
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||||
id="autopilotInput"
|
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||||
key="auto pilot throughput input"
|
Minimum RU/s
|
||||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
</Text>
|
||||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
|
||||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
</Stack>
|
||||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
<Text
|
||||||
onChange={this.onAutoPilotThroughputChange}
|
style={{
|
||||||
min={autoPilotThroughput1K}
|
fontFamily: "Segoe UI",
|
||||||
onGetErrorMessage={(value: string) => {
|
width: 70,
|
||||||
const sanitizedValue = getSanitizedInputValue(value);
|
height: 28,
|
||||||
return sanitizedValue % 1000
|
border: "none",
|
||||||
? "Throughput value must be in increments of 1000"
|
fontSize: 14,
|
||||||
: this.props.throughputError;
|
backgroundColor: "transparent",
|
||||||
}}
|
fontWeight: 400,
|
||||||
validateOnLoad={false}
|
display: "flex",
|
||||||
/>
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Column 2: "x 10 =" Text */}
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: "Segoe UI",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
paddingBottom: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
x 10 =
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Column 3: Maximum RU/s */}
|
||||||
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
|
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||||
|
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||||
|
Maximum RU/s
|
||||||
|
</Text>
|
||||||
|
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
|
||||||
|
</Stack>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
id="autopilotInput"
|
||||||
|
key="auto pilot throughput input"
|
||||||
|
styles={{
|
||||||
|
...getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline),
|
||||||
|
fieldGroup: { width: 100, height: 28 },
|
||||||
|
field: { fontSize: 14, fontWeight: 400 },
|
||||||
|
}}
|
||||||
|
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||||
|
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||||
|
value={
|
||||||
|
this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()
|
||||||
|
}
|
||||||
|
onChange={this.onAutoPilotThroughputChange}
|
||||||
|
min={autoPilotThroughput1K}
|
||||||
|
onGetErrorMessage={(value: string) => {
|
||||||
|
const sanitizedValue = getSanitizedInputValue(value);
|
||||||
|
return sanitizedValue % 1000
|
||||||
|
? "Throughput value must be in increments of 1000"
|
||||||
|
: this.props.throughputError;
|
||||||
|
}}
|
||||||
|
validateOnLoad={false}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -157,35 +157,148 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StyledTextFieldBase
|
<Stack
|
||||||
disabled={true}
|
horizontal={true}
|
||||||
id="autopilotInput"
|
tokens={
|
||||||
key="auto pilot throughput input"
|
|
||||||
label="Maximum RU/s required by this resource"
|
|
||||||
min={1000}
|
|
||||||
onChange={[Function]}
|
|
||||||
onGetErrorMessage={[Function]}
|
|
||||||
required={true}
|
|
||||||
step={1000}
|
|
||||||
styles={
|
|
||||||
{
|
{
|
||||||
"fieldGroup": {
|
"childrenGap": 8,
|
||||||
"borderColor": "",
|
|
||||||
"height": 25,
|
|
||||||
"selectors": {
|
|
||||||
":disabled": {
|
|
||||||
"backgroundColor": undefined,
|
|
||||||
"borderColor": undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"width": 300,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type="number"
|
verticalAlign="end"
|
||||||
validateOnLoad={false}
|
>
|
||||||
value=""
|
<Stack
|
||||||
/>
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"fontWeight": 600,
|
||||||
|
"lineHeight": "20px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
Minimum RU/s
|
||||||
|
</Text>
|
||||||
|
<FontIcon
|
||||||
|
iconName="Info"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "#666",
|
||||||
|
"fontSize": 12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"alignItems": "center",
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"border": "none",
|
||||||
|
"boxSizing": "border-box",
|
||||||
|
"display": "flex",
|
||||||
|
"fontFamily": "Segoe UI",
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": 400,
|
||||||
|
"height": 28,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"width": 70,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
400
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"fontFamily": "Segoe UI",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 400,
|
||||||
|
"paddingBottom": 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
x 10 =
|
||||||
|
</Text>
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"fontWeight": 600,
|
||||||
|
"lineHeight": "20px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
Maximum RU/s
|
||||||
|
</Text>
|
||||||
|
<FontIcon
|
||||||
|
iconName="Info"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "#666",
|
||||||
|
"fontSize": 12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
disabled={true}
|
||||||
|
id="autopilotInput"
|
||||||
|
key="auto pilot throughput input"
|
||||||
|
min={1000}
|
||||||
|
onChange={[Function]}
|
||||||
|
onGetErrorMessage={[Function]}
|
||||||
|
required={true}
|
||||||
|
step={1000}
|
||||||
|
styles={
|
||||||
|
{
|
||||||
|
"field": {
|
||||||
|
"fontSize": 14,
|
||||||
|
"fontWeight": 400,
|
||||||
|
},
|
||||||
|
"fieldGroup": {
|
||||||
|
"height": 28,
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="number"
|
||||||
|
validateOnLoad={false}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { useDatabases } from "Explorer/useDatabases";
|
|||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||||
|
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
import * as SharedConstants from "../../../Shared/Constants";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||||
import "./ThroughputInput.less";
|
import "./ThroughputInput.less";
|
||||||
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
|
||||||
|
|
||||||
export interface ThroughputInputProps {
|
export interface ThroughputInputProps {
|
||||||
isDatabase: boolean;
|
isDatabase: boolean;
|
||||||
@@ -41,11 +41,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
let defaultThroughput: number;
|
let defaultThroughput: number;
|
||||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
const workloadType: Constants.WorkloadType = getWorkloadType();
|
||||||
|
|
||||||
if (
|
if (isFabricNative()) {
|
||||||
|
defaultThroughput = AutoPilotUtils.autoPilotThroughput5K;
|
||||||
|
} else if (
|
||||||
isFreeTier ||
|
isFreeTier ||
|
||||||
isQuickstart ||
|
isQuickstart ||
|
||||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType) ||
|
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
||||||
isFabricNative()
|
|
||||||
) {
|
) {
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
||||||
} else if (workloadType === Constants.WorkloadType.Production) {
|
} else if (workloadType === Constants.WorkloadType.Production) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
|
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
|
||||||
@@ -90,12 +91,13 @@ export class ContainerSampleGenerator {
|
|||||||
}
|
}
|
||||||
const { databaseAccount: account } = userContext;
|
const { databaseAccount: account } = userContext;
|
||||||
const databaseId = collection.databaseId;
|
const databaseId = collection.databaseId;
|
||||||
|
|
||||||
const gremlinClient = new GremlinClient();
|
const gremlinClient = new GremlinClient();
|
||||||
gremlinClient.initialize({
|
gremlinClient.initialize({
|
||||||
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
|
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
|
||||||
databaseId: databaseId,
|
databaseId: databaseId,
|
||||||
collectionId: collection.id(),
|
collectionId: collection.id(),
|
||||||
masterKey: userContext.masterKey || "",
|
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||||
maxResultSize: 100,
|
maxResultSize: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
|
|||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
import {
|
||||||
|
isFabricMirrored,
|
||||||
|
isFabricMirroredKey,
|
||||||
|
isFabricNative,
|
||||||
|
scheduleRefreshFabricToken,
|
||||||
|
} from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
@@ -284,14 +289,40 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openInVsCode(): void {
|
/**
|
||||||
|
* Generates a VS Code DocumentDB connection URL using the current user's MongoDB connection parameters.
|
||||||
|
* Double-encodes the updated connection string for safe usage in VS Code URLs.
|
||||||
|
*
|
||||||
|
* The DocumentDB VS Code extension requires double encoding for connection strings.
|
||||||
|
* See: https://microsoft.github.io/vscode-documentdb/manual/how-to-construct-url.html#double-encoding
|
||||||
|
*
|
||||||
|
* @returns {string} The encoded VS Code DocumentDB connection URL.
|
||||||
|
*/
|
||||||
|
private getDocumentDbUrl() {
|
||||||
|
const { adminLogin: adminLoginuserName = "", connectionString = "" } = userContext.vcoreMongoConnectionParams;
|
||||||
|
const updatedConnectionString = connectionString.replace(/<(user|username)>:<password>/i, adminLoginuserName);
|
||||||
|
const encodedUpdatedConnectionString = encodeURIComponent(encodeURIComponent(updatedConnectionString));
|
||||||
|
const documentDbUrl = `vscode://ms-azuretools.vscode-documentdb?connectionString=${encodedUpdatedConnectionString}`;
|
||||||
|
return documentDbUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCosmosDbUrl() {
|
||||||
const activeTab = useTabs.getState().activeTab;
|
const activeTab = useTabs.getState().activeTab;
|
||||||
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
|
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
|
||||||
const database = encodeURIComponent(activeTab?.collection?.databaseId);
|
const database = encodeURIComponent(activeTab?.collection?.databaseId);
|
||||||
const container = encodeURIComponent(activeTab?.collection?.id());
|
const container = encodeURIComponent(activeTab?.collection?.id());
|
||||||
const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
|
const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
|
||||||
const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
|
const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
|
||||||
|
return vscodeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVSCodeUrl(): string {
|
||||||
|
const isvCore = (userContext.apiType || userContext.databaseAccount.kind) === "VCoreMongo";
|
||||||
|
return isvCore ? this.getDocumentDbUrl() : this.getCosmosDbUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
public openInVsCode(): void {
|
||||||
|
const vscodeUrl = this.getVSCodeUrl();
|
||||||
const openVSCodeDialogProps: DialogProps = {
|
const openVSCodeDialogProps: DialogProps = {
|
||||||
linkProps: {
|
linkProps: {
|
||||||
linkText: "Download Visual Studio Code",
|
linkText: "Download Visual Studio Code",
|
||||||
@@ -1149,7 +1180,10 @@ export default class Explorer {
|
|||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
||||||
}
|
}
|
||||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
|
||||||
|
if (!isFabricNative()) {
|
||||||
|
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||||
const isNotebookEnabled =
|
const isNotebookEnabled =
|
||||||
@@ -1171,7 +1205,7 @@ export default class Explorer {
|
|||||||
await this.initNotebooks(userContext.databaseAccount);
|
await this.initNotebooks(userContext.databaseAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL") {
|
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()) {
|
||||||
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
|
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
|
||||||
updateUserContext({ throughputBucketsEnabled });
|
updateUserContext({ throughputBucketsEnabled });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,8 +163,7 @@ describe("GraphExplorer", () => {
|
|||||||
graphBackendEndpoint: "graphBackendEndpoint",
|
graphBackendEndpoint: "graphBackendEndpoint",
|
||||||
databaseId: "databaseId",
|
databaseId: "databaseId",
|
||||||
collectionId: "collectionId",
|
collectionId: "collectionId",
|
||||||
masterKey: "masterKey",
|
password: "password",
|
||||||
|
|
||||||
onLoadStartKey: 0,
|
onLoadStartKey: 0,
|
||||||
onLoadStartKeyChange: (newKey: number): void => {},
|
onLoadStartKeyChange: (newKey: number): void => {},
|
||||||
resourceId: "resourceId",
|
resourceId: "resourceId",
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export interface GraphExplorerProps {
|
|||||||
graphBackendEndpoint: string;
|
graphBackendEndpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
masterKey: string;
|
password: string;
|
||||||
|
|
||||||
onLoadStartKey: number;
|
onLoadStartKey: number;
|
||||||
onLoadStartKeyChange: (newKey: number) => void;
|
onLoadStartKeyChange: (newKey: number) => void;
|
||||||
@@ -1300,7 +1300,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
endpoint: `wss://${this.props.graphBackendEndpoint}`,
|
endpoint: `wss://${this.props.graphBackendEndpoint}`,
|
||||||
databaseId: this.props.databaseId,
|
databaseId: this.props.databaseId,
|
||||||
collectionId: this.props.collectionId,
|
collectionId: this.props.collectionId,
|
||||||
masterKey: this.props.masterKey,
|
password: this.props.password,
|
||||||
maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
|
maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,28 +8,28 @@ describe("Gremlin Client", () => {
|
|||||||
endpoint: null,
|
endpoint: null,
|
||||||
collectionId: null,
|
collectionId: null,
|
||||||
databaseId: null,
|
databaseId: null,
|
||||||
masterKey: null,
|
|
||||||
maxResultSize: 10000,
|
maxResultSize: 10000,
|
||||||
|
password: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should use databaseId, collectionId and masterKey to authenticate", () => {
|
it("should use databaseId, collectionId and password to authenticate", () => {
|
||||||
const collectionId = "collectionId";
|
const collectionId = "collectionId";
|
||||||
const databaseId = "databaseId";
|
const databaseId = "databaseId";
|
||||||
const masterKey = "masterKey";
|
const testPassword = "password";
|
||||||
const gremlinClient = new GremlinClient();
|
const gremlinClient = new GremlinClient();
|
||||||
|
|
||||||
gremlinClient.initialize({
|
gremlinClient.initialize({
|
||||||
endpoint: null,
|
endpoint: null,
|
||||||
collectionId,
|
collectionId,
|
||||||
databaseId,
|
databaseId,
|
||||||
masterKey,
|
|
||||||
maxResultSize: 0,
|
maxResultSize: 0,
|
||||||
|
password: testPassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
// User must includes these values
|
// User must includes these values
|
||||||
expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1);
|
expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1);
|
||||||
expect(gremlinClient.client.params.user.indexOf(databaseId)).not.toBe(-1);
|
expect(gremlinClient.client.params.user.indexOf(databaseId)).not.toBe(-1);
|
||||||
expect(gremlinClient.client.params.password).toEqual(masterKey);
|
expect(gremlinClient.client.params.password).toEqual(testPassword);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should aggregate RU charges across multiple responses", (done) => {
|
it("should aggregate RU charges across multiple responses", (done) => {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export interface GremlinClientParameters {
|
|||||||
endpoint: string;
|
endpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
masterKey: string;
|
|
||||||
maxResultSize: number;
|
maxResultSize: number;
|
||||||
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GremlinRequestResult {
|
export interface GremlinRequestResult {
|
||||||
@@ -43,7 +43,7 @@ export class GremlinClient {
|
|||||||
this.client = new GremlinSimpleClient({
|
this.client = new GremlinSimpleClient({
|
||||||
endpoint: params.endpoint,
|
endpoint: params.endpoint,
|
||||||
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
|
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
|
||||||
password: params.masterKey,
|
password: params.password,
|
||||||
successCallback: (result: Result) => {
|
successCallback: (result: Result) => {
|
||||||
this.storePendingResult(result);
|
this.storePendingResult(result);
|
||||||
this.flushResult(result.requestId);
|
this.flushResult(result.requestId);
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
import * as sinon from "sinon";
|
import * as sinon from "sinon";
|
||||||
import {
|
import {
|
||||||
|
GremlinRequestMessage,
|
||||||
|
GremlinResponseMessage,
|
||||||
GremlinSimpleClient,
|
GremlinSimpleClient,
|
||||||
GremlinSimpleClientParameters,
|
GremlinSimpleClientParameters,
|
||||||
Result,
|
Result,
|
||||||
GremlinRequestMessage,
|
|
||||||
GremlinResponseMessage,
|
|
||||||
} from "./GremlinSimpleClient";
|
} from "./GremlinSimpleClient";
|
||||||
|
|
||||||
describe("Gremlin Simple Client", () => {
|
describe("Gremlin Simple Client", () => {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import { getCollectionName } from "Utils/APITypeUtils";
|
|||||||
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||||
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||||
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
||||||
@@ -60,7 +61,6 @@ import { useDatabases } from "../../useDatabases";
|
|||||||
import { PanelFooterComponent } from "../PanelFooterComponent";
|
import { PanelFooterComponent } from "../PanelFooterComponent";
|
||||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
|
||||||
|
|
||||||
export interface AddCollectionPanelProps {
|
export interface AddCollectionPanelProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
@@ -123,7 +123,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
isSharded: userContext.apiType !== "Tables",
|
isSharded: userContext.apiType !== "Tables",
|
||||||
partitionKey: getPartitionKey(props.isQuickstart),
|
partitionKey: getPartitionKey(props.isQuickstart),
|
||||||
subPartitionKeys: [],
|
subPartitionKeys: [],
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: isFabricNative(), // Dedicated throughput is only enabled in Fabric Native by default
|
||||||
createMongoWildCardIndex:
|
createMongoWildCardIndex:
|
||||||
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
||||||
useHashV1: false,
|
useHashV1: false,
|
||||||
@@ -406,9 +406,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
responsiveMode={999}
|
responsiveMode={999}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
|
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
|
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
|
||||||
@@ -448,8 +448,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
this.setState({ collectionId: event.target.value })
|
this.setState({ collectionId: event.target.value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
|
|
||||||
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
|
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
|
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
|
||||||
@@ -644,7 +645,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||||
@@ -708,7 +709,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.shouldShowCollectionThroughputInput() && (
|
{this.shouldShowCollectionThroughputInput() && !isFabricNative() && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={false}
|
isDatabase={false}
|
||||||
@@ -775,7 +776,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
|
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||||
|
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
|
||||||
|
)}
|
||||||
|
|
||||||
{shouldShowAnalyticalStoreOptions() && (
|
{shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
|
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
|
||||||
@@ -1131,7 +1134,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
private shouldShowCollectionThroughputInput(): boolean {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (isFabricNative() || isServerlessAccount()) {
|
if (isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1352,8 +1355,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
// Throughput
|
// Throughput
|
||||||
if (isFabricNative()) {
|
if (isFabricNative()) {
|
||||||
// Fabric Native accounts are always autoscale and have a fixed throughput of 1K
|
// Fabric Native accounts are always autoscale and have a fixed throughput of 5K
|
||||||
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput5K;
|
||||||
offerThroughput = undefined;
|
offerThroughput = undefined;
|
||||||
} else if (databaseLevelThroughput) {
|
} else if (databaseLevelThroughput) {
|
||||||
if (this.state.createNewDatabase) {
|
if (this.state.createNewDatabase) {
|
||||||
|
|||||||
@@ -142,16 +142,16 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
<Separator
|
||||||
<Separator
|
className="panelSeparator"
|
||||||
className="panelSeparator"
|
style={
|
||||||
style={
|
{
|
||||||
{
|
"marginBottom": -4,
|
||||||
"marginBottom": -4,
|
"marginTop": -4,
|
||||||
"marginTop": -4,
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
@@ -202,16 +202,16 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</Stack>
|
<Separator
|
||||||
<Separator
|
className="panelSeparator"
|
||||||
className="panelSeparator"
|
style={
|
||||||
style={
|
{
|
||||||
{
|
"marginBottom": -5,
|
||||||
"marginBottom": -5,
|
"marginTop": -5,
|
||||||
"marginTop": -5,
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
|
|||||||
@@ -433,9 +433,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logConsoleInfo(
|
|
||||||
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
|
||||||
);
|
|
||||||
refreshExplorer && (await explorer.refreshExplorer());
|
refreshExplorer && (await explorer.refreshExplorer());
|
||||||
closeSidePanel();
|
closeSidePanel();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { formatErrorMessage, formatInfoMessage, formatWarningMessage } from "./U
|
|||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const DEFAULT_CLOUDSHELL_REGION = "westus";
|
const DEFAULT_CLOUDSHELL_REGION = "westus";
|
||||||
|
const DEFAULT_FAIRFAX_CLOUDSHELL_REGION = "usgovvirginia";
|
||||||
const POLLING_INTERVAL_MS = 2000;
|
const POLLING_INTERVAL_MS = 2000;
|
||||||
const MAX_RETRY_COUNT = 10;
|
const MAX_RETRY_COUNT = 10;
|
||||||
const MAX_PING_COUNT = 120 * 60; // 120 minutes (60 seconds/minute)
|
const MAX_PING_COUNT = 120 * 60; // 120 minutes (60 seconds/minute)
|
||||||
@@ -44,32 +45,26 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
|
|||||||
|
|
||||||
resolvedRegion = determineCloudShellRegion();
|
resolvedRegion = determineCloudShellRegion();
|
||||||
|
|
||||||
resolvedRegion = determineCloudShellRegion();
|
|
||||||
|
|
||||||
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
|
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
|
||||||
terminal.writeln(
|
terminal.writeln(
|
||||||
formatInfoMessage(
|
formatInfoMessage(
|
||||||
"The Cloud Shell environment will operate in a region that may differ from your database's region.",
|
"The Cloud Shell environment will operate in a region that may differ from your database's region.",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
terminal.writeln(formatInfoMessage("This has two potential implications:"));
|
terminal.writeln(formatInfoMessage("By using this feature, you acknowledge and agree to the following"));
|
||||||
terminal.writeln(formatInfoMessage("1. Performance Impact:"));
|
terminal.writeln(formatInfoMessage("1. Performance Impact:"));
|
||||||
terminal.writeln(
|
terminal.writeln(
|
||||||
formatInfoMessage(" Commands may experience higher latency due to geographic distance between regions."),
|
formatInfoMessage(" Commands may experience higher latency due to geographic distance between regions."),
|
||||||
);
|
);
|
||||||
terminal.writeln(formatInfoMessage("2. Data Compliance Considerations:"));
|
terminal.writeln(formatInfoMessage("2. Data Transfers:"));
|
||||||
terminal.writeln(
|
terminal.writeln(
|
||||||
formatInfoMessage(
|
formatInfoMessage(
|
||||||
" Data processed through this shell could temporarily reside in a different geographic region,",
|
" Data processed through this Cloud Shell service can be processed outside of your tenant's geographical region, compliance boundary or national cloud instance.",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
terminal.writeln(
|
|
||||||
formatInfoMessage(" which may affect compliance with data residency requirements or regulations specific"),
|
|
||||||
);
|
|
||||||
terminal.writeln(formatInfoMessage(" to your organization."));
|
|
||||||
terminal.writeln("");
|
terminal.writeln("");
|
||||||
|
|
||||||
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data governance and compliance, please visit:");
|
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data residency, please visit:");
|
||||||
terminal.writeln("\x1b[94mhttps://learn.microsoft.com/en-us/azure/cosmos-db/data-residency\x1b[0m");
|
terminal.writeln("\x1b[94mhttps://learn.microsoft.com/en-us/azure/cosmos-db/data-residency\x1b[0m");
|
||||||
|
|
||||||
// Ask for user consent for region
|
// Ask for user consent for region
|
||||||
@@ -159,7 +154,9 @@ export const ensureCloudShellProviderRegistered = async (): Promise<void> => {
|
|||||||
* Determines the appropriate CloudShell region
|
* Determines the appropriate CloudShell region
|
||||||
*/
|
*/
|
||||||
export const determineCloudShellRegion = (): string => {
|
export const determineCloudShellRegion = (): string => {
|
||||||
return getNormalizedRegion(userContext.databaseAccount?.location, DEFAULT_CLOUDSHELL_REGION);
|
const defaultRegion =
|
||||||
|
userContext.portalEnv === "fairfax" ? DEFAULT_FAIRFAX_CLOUDSHELL_REGION : DEFAULT_CLOUDSHELL_REGION;
|
||||||
|
return getNormalizedRegion(userContext.databaseAccount?.location, defaultRegion);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -258,14 +258,7 @@ Key limitations:
|
|||||||
|
|
||||||
### Data Residency
|
### Data Residency
|
||||||
|
|
||||||
Data residency requirements may not be fully satisfied when using CloudShell due to limited regional availability. CloudShell services are currently available in the following regions:
|
Data residency requirements may not be fully satisfied when using CloudShell due to limited regional availability.
|
||||||
|
|
||||||
| Geography | Regions |
|
|
||||||
|-----------|---------|
|
|
||||||
| Americas | East US, West US 2, South Central US, West Central US |
|
|
||||||
| Europe | West Europe, North Europe |
|
|
||||||
| Asia Pacific | Southeast Asia, Japan East, Australia East |
|
|
||||||
| Middle East | UAE North |
|
|
||||||
|
|
||||||
**Note:** For up-to-date supported regions, refer to the region configuration in:
|
**Note:** For up-to-date supported regions, refer to the region configuration in:
|
||||||
`src/Explorer/CloudShell/Configuration/RegionConfig.ts`
|
`src/Explorer/CloudShell/Configuration/RegionConfig.ts`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AbstractShellHandler, DISABLE_HISTORY, START_MARKER, EXIT_COMMAND } from "./AbstractShellHandler";
|
import { AbstractShellHandler, DISABLE_HISTORY, EXIT_COMMAND, START_MARKER } from "./AbstractShellHandler";
|
||||||
|
|
||||||
// Mock implementation for testing
|
// Mock implementation for testing
|
||||||
class MockShellHandler extends AbstractShellHandler {
|
class MockShellHandler extends AbstractShellHandler {
|
||||||
@@ -18,8 +18,8 @@ class MockShellHandler extends AbstractShellHandler {
|
|||||||
return "mock-endpoint";
|
return "mock-endpoint";
|
||||||
}
|
}
|
||||||
|
|
||||||
getTerminalSuppressedData(): string {
|
getTerminalSuppressedData(): string[] {
|
||||||
return "suppressed-data";
|
return ["suppressed-data"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ describe("AbstractShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return the terminal suppressed data", () => {
|
it("should return the terminal suppressed data", () => {
|
||||||
expect(shellHandler.getTerminalSuppressedData()).toBe("suppressed-data");
|
expect(shellHandler.getTerminalSuppressedData()).toEqual(["suppressed-data"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ export const DISABLE_HISTORY = `set +o history`;
|
|||||||
* Command that displays an error message and exits the shell session.
|
* Command that displays an error message and exits the shell session.
|
||||||
* Used when shell initialization or connection fails.
|
* Used when shell initialization or connection fails.
|
||||||
*/
|
*/
|
||||||
export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && exit`;
|
export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && disown -a && exit`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This command runs mongosh in no-database and quiet mode,
|
||||||
|
* and evaluates the `disableTelemetry()` function to turn off telemetry collection.
|
||||||
|
*/
|
||||||
|
export const DISABLE_TELEMETRY_COMMAND = `mongosh --nodb --quiet --eval "disableTelemetry()"`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class that defines the interface for shell-specific handlers
|
* Abstract class that defines the interface for shell-specific handlers
|
||||||
@@ -31,7 +37,8 @@ export abstract class AbstractShellHandler {
|
|||||||
abstract getShellName(): string;
|
abstract getShellName(): string;
|
||||||
abstract getSetUpCommands(): string[];
|
abstract getSetUpCommands(): string[];
|
||||||
abstract getConnectionCommand(): string;
|
abstract getConnectionCommand(): string;
|
||||||
abstract getTerminalSuppressedData(): string;
|
abstract getTerminalSuppressedData(): string[];
|
||||||
|
updateTerminalData?(data: string): string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs the complete initialization command sequence for the shell.
|
* Constructs the complete initialization command sequence for the shell.
|
||||||
@@ -77,7 +84,7 @@ export abstract class AbstractShellHandler {
|
|||||||
* is not already present in the environment.
|
* is not already present in the environment.
|
||||||
*/
|
*/
|
||||||
protected mongoShellSetupCommands(): string[] {
|
protected mongoShellSetupCommands(): string[] {
|
||||||
const PACKAGE_VERSION: string = "2.5.0";
|
const PACKAGE_VERSION: string = "2.5.5";
|
||||||
return [
|
return [
|
||||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
||||||
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||||
@@ -85,7 +92,7 @@ export abstract class AbstractShellHandler {
|
|||||||
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
|
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
|
||||||
`if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
`if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
||||||
"source ~/.bashrc",
|
"if ! command -v mongosh &> /dev/null; then source ~/.bashrc; fi",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ describe("CassandraShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should return the correct terminal suppressed data", () => {
|
test("should return the correct terminal suppressed data", () => {
|
||||||
expect(handler.getTerminalSuppressedData()).toBe("");
|
expect(handler.getTerminalSuppressedData()).toEqual([""]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should include the correct package version in setup commands", () => {
|
test("should include the correct package version in setup commands", () => {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class CassandraShellHandler extends AbstractShellHandler {
|
|||||||
return `cqlsh ${getHostFromUrl(this._endpoint)} 10350 -u ${dbName} -p ${this._key} --ssl`;
|
return `cqlsh ${getHostFromUrl(this._endpoint)} 10350 -u ${dbName} -p ${this._key} --ssl`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "";
|
return [""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ jest.mock("../../../../UserContext", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../Utils/CommonUtils", () => ({
|
jest.mock("../Utils/CommonUtils", () => ({
|
||||||
|
...jest.requireActual("../Utils/CommonUtils"),
|
||||||
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
|
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ describe("MongoShellHandler", () => {
|
|||||||
|
|
||||||
expect(Array.isArray(commands)).toBe(true);
|
expect(Array.isArray(commands)).toBe(true);
|
||||||
expect(commands.length).toBe(7);
|
expect(commands.length).toBe(7);
|
||||||
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
expect(commands[1]).toContain("mongosh-2.5.5-linux-x64.tgz");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ describe("MongoShellHandler", () => {
|
|||||||
const command = mongoShellHandler.getConnectionCommand();
|
const command = mongoShellHandler.getConnectionCommand();
|
||||||
|
|
||||||
expect(command).toBe(
|
expect(command).toBe(
|
||||||
"mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
|
'mongosh --nodb --quiet --eval "disableTelemetry()" && mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates',
|
||||||
);
|
);
|
||||||
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");
|
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");
|
||||||
|
|
||||||
@@ -124,7 +125,10 @@ describe("MongoShellHandler", () => {
|
|||||||
|
|
||||||
describe("getTerminalSuppressedData", () => {
|
describe("getTerminalSuppressedData", () => {
|
||||||
it("should return the correct warning message", () => {
|
it("should return the correct warning message", () => {
|
||||||
expect(mongoShellHandler.getTerminalSuppressedData()).toBe("Warning: Non-Genuine MongoDB Detected");
|
expect(mongoShellHandler.getTerminalSuppressedData()).toEqual([
|
||||||
|
"Warning: Non-Genuine MongoDB Detected",
|
||||||
|
"Telemetry is now disabled.",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
import { filterAndCleanTerminalOutput, getHostFromUrl, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } from "./AbstractShellHandler";
|
||||||
|
|
||||||
export class MongoShellHandler extends AbstractShellHandler {
|
export class MongoShellHandler extends AbstractShellHandler {
|
||||||
private _key: string;
|
private _key: string;
|
||||||
private _endpoint: string | undefined;
|
private _endpoint: string | undefined;
|
||||||
|
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
|
||||||
constructor(private key: string) {
|
constructor(private key: string) {
|
||||||
super();
|
super();
|
||||||
this._key = key;
|
this._key = key;
|
||||||
@@ -29,6 +30,8 @@ export class MongoShellHandler extends AbstractShellHandler {
|
|||||||
return "echo 'Database name not found.'";
|
return "echo 'Database name not found.'";
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
DISABLE_TELEMETRY_COMMAND +
|
||||||
|
" && " +
|
||||||
"mongosh mongodb://" +
|
"mongosh mongodb://" +
|
||||||
getHostFromUrl(this._endpoint) +
|
getHostFromUrl(this._endpoint) +
|
||||||
":10255?appName=" +
|
":10255?appName=" +
|
||||||
@@ -41,7 +44,11 @@ export class MongoShellHandler extends AbstractShellHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "Warning: Non-Genuine MongoDB Detected";
|
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTerminalData(data: string): string {
|
||||||
|
return filterAndCleanTerminalOutput(data, this._removeInfoText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ describe("PostgresShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return empty string for terminal suppressed data", () => {
|
it("should return empty string for terminal suppressed data", () => {
|
||||||
expect(postgresShellHandler.getTerminalSuppressedData()).toBe("");
|
expect(postgresShellHandler.getTerminalSuppressedData()).toEqual([""]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class PostgresShellHandler extends AbstractShellHandler {
|
|||||||
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require --set=application_name=${this.APP_NAME}`;
|
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require --set=application_name=${this.APP_NAME}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "";
|
return [""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe("VCoreMongoShellHandler", () => {
|
|||||||
|
|
||||||
expect(Array.isArray(commands)).toBe(true);
|
expect(Array.isArray(commands)).toBe(true);
|
||||||
expect(commands.length).toBe(7);
|
expect(commands.length).toBe(7);
|
||||||
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
expect(commands[1]).toContain("mongosh-2.5.5-linux-x64.tgz");
|
||||||
expect(commands[0]).toContain("mongosh not found");
|
expect(commands[0]).toContain("mongosh not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,7 +57,10 @@ describe("VCoreMongoShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return the correct terminal suppressed data", () => {
|
it("should return the correct terminal suppressed data", () => {
|
||||||
expect(vcoreMongoShellHandler.getTerminalSuppressedData()).toBe("Warning: Non-Genuine MongoDB Detected");
|
expect(vcoreMongoShellHandler.getTerminalSuppressedData()).toEqual([
|
||||||
|
"Warning: Non-Genuine MongoDB Detected",
|
||||||
|
"Telemetry is now disabled.",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||||
|
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } from "./AbstractShellHandler";
|
||||||
|
|
||||||
export class VCoreMongoShellHandler extends AbstractShellHandler {
|
export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||||
private _endpoint: string | undefined;
|
private _endpoint: string | undefined;
|
||||||
|
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -23,10 +25,17 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
|
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
|
||||||
return `mongosh "mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000&appName=${this.APP_NAME}"`;
|
|
||||||
|
const connectionUri = `mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000&appName=${this.APP_NAME}`;
|
||||||
|
|
||||||
|
return `${DISABLE_TELEMETRY_COMMAND} && mongosh "${connectionUri}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "Warning: Non-Genuine MongoDB Detected";
|
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTerminalData(data: string): string {
|
||||||
|
return filterAndCleanTerminalOutput(data, this._removeInfoText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,11 +135,17 @@ export class AttachAddon implements ITerminalAddon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this._allowTerminalWrite) {
|
if (this._allowTerminalWrite) {
|
||||||
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
|
const updatedData =
|
||||||
const hasSuppressedData = suppressedData && suppressedData.length > 0;
|
typeof this._shellHandler?.updateTerminalData === "function"
|
||||||
|
? this._shellHandler.updateTerminalData(data)
|
||||||
|
: data;
|
||||||
|
|
||||||
if (!hasSuppressedData || !data.includes(suppressedData)) {
|
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
|
||||||
terminal.write(data);
|
|
||||||
|
const shouldNotWrite = suppressedData.filter(Boolean).some((item) => updatedData.includes(item));
|
||||||
|
|
||||||
|
if (!shouldNotWrite) {
|
||||||
|
terminal.write(updatedData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
71
src/Explorer/Tabs/CloudShellTab/Utils/CloudShellIPUtils.ts
Normal file
71
src/Explorer/Tabs/CloudShellTab/Utils/CloudShellIPUtils.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { userContext } from "../../../../UserContext";
|
||||||
|
|
||||||
|
export const CLOUDSHELL_IP_RECOMMENDATIONS = {
|
||||||
|
centralindia: [
|
||||||
|
{ startIP: "4.247.135.109", endIP: "4.247.135.109" },
|
||||||
|
{ startIP: "74.225.207.63", endIP: "74.225.207.63" },
|
||||||
|
],
|
||||||
|
southeastasia: [{ startIP: "4.194.5.74", endIP: "4.194.213.10" }],
|
||||||
|
centraluseuap: [
|
||||||
|
{ startIP: "52.158.186.182", endIP: "52.158.186.182" },
|
||||||
|
{ startIP: "172.215.26.246", endIP: "172.215.26.246" },
|
||||||
|
{ startIP: "134.138.154.177", endIP: "134.138.154.177" },
|
||||||
|
{ startIP: "134.138.129.52", endIP: "134.138.129.52" },
|
||||||
|
{ startIP: "172.215.31.177", endIP: "172.215.31.177" },
|
||||||
|
],
|
||||||
|
eastus2euap: [
|
||||||
|
{ startIP: "135.18.43.51", endIP: "135.18.43.51" },
|
||||||
|
{ startIP: "20.252.175.33", endIP: "20.252.175.33" },
|
||||||
|
{ startIP: "40.89.88.111", endIP: "40.89.88.111" },
|
||||||
|
{ startIP: "135.18.17.187", endIP: "135.18.17.187" },
|
||||||
|
{ startIP: "135.18.67.251", endIP: "135.18.67.251" },
|
||||||
|
],
|
||||||
|
eastus: [
|
||||||
|
{ startIP: "40.71.199.151", endIP: "40.71.199.151" },
|
||||||
|
{ startIP: "20.42.18.188", endIP: "20.42.18.188" },
|
||||||
|
{ startIP: "52.190.17.9", endIP: "52.190.17.9" },
|
||||||
|
{ startIP: "20.120.96.152", endIP: "20.120.96.152" },
|
||||||
|
],
|
||||||
|
northeurope: [
|
||||||
|
{ startIP: "74.234.65.146", endIP: "74.234.65.146" },
|
||||||
|
{ startIP: "52.169.70.113", endIP: "52.169.70.113" },
|
||||||
|
],
|
||||||
|
southcentralus: [
|
||||||
|
{ startIP: "4.151.247.81", endIP: "4.151.247.81" },
|
||||||
|
{ startIP: "20.225.211.35", endIP: "20.225.211.35" },
|
||||||
|
{ startIP: "4.151.48.133", endIP: "4.151.48.133" },
|
||||||
|
{ startIP: "4.151.247.225", endIP: "4.151.247.225" },
|
||||||
|
],
|
||||||
|
westeurope: [
|
||||||
|
{ startIP: "52.166.126.216", endIP: "52.166.126.216" },
|
||||||
|
{ startIP: "108.142.162.20", endIP: "108.142.162.20" },
|
||||||
|
{ startIP: "52.178.13.125", endIP: "52.178.13.125" },
|
||||||
|
{ startIP: "172.201.33.160", endIP: "172.201.33.160" },
|
||||||
|
],
|
||||||
|
westus: [
|
||||||
|
{ startIP: "20.245.161.131", endIP: "20.245.161.131" },
|
||||||
|
{ startIP: "57.154.182.51", endIP: "57.154.182.51" },
|
||||||
|
{ startIP: "40.118.133.244", endIP: "40.118.133.244" },
|
||||||
|
{ startIP: "20.253.192.12", endIP: "20.253.192.12" },
|
||||||
|
{ startIP: "20.43.245.209", endIP: "20.43.245.209" },
|
||||||
|
{ startIP: "20.66.22.66", endIP: "20.66.22.66" },
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface CloudShellIPRange {
|
||||||
|
startIP: string;
|
||||||
|
endIP: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCloudShellIPsForRegion(region: string): readonly CloudShellIPRange[] {
|
||||||
|
const normalizedRegion = region.toLowerCase();
|
||||||
|
return CLOUDSHELL_IP_RECOMMENDATIONS[normalizedRegion as keyof typeof CLOUDSHELL_IP_RECOMMENDATIONS] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClusterRegion(): string {
|
||||||
|
const location = userContext?.databaseAccount?.location;
|
||||||
|
if (location) {
|
||||||
|
return location.toLowerCase();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
@@ -50,3 +50,34 @@ export const getShellNameForDisplay = (terminalKind: TerminalKind): string => {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get MongoDB shell information text that should be removed from terminal output
|
||||||
|
*/
|
||||||
|
export const getMongoShellRemoveInfoText = (): string[] => {
|
||||||
|
return [
|
||||||
|
"For mongosh info see: https://www.mongodb.com/docs/mongodb-shell/",
|
||||||
|
"disableTelemetry() command",
|
||||||
|
"https://www.mongodb.com/legal/privacy-policy",
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterAndCleanTerminalOutput = (data: string, removeInfoText: string[]): string => {
|
||||||
|
if (!data || removeInfoText.length === 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = data.split("\n");
|
||||||
|
const filteredLines: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const shouldRemove = removeInfoText.some((text) => line.includes(text));
|
||||||
|
|
||||||
|
if (!shouldRemove) {
|
||||||
|
filteredLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredLines.join("\n").replace(/((\r\n)|\n|\r){2,}/g, "\r\n");
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const validCloudShellRegions = new Set([
|
|||||||
"centralindia",
|
"centralindia",
|
||||||
"southeastasia",
|
"southeastasia",
|
||||||
"westcentralus",
|
"westcentralus",
|
||||||
|
"usgovvirginia",
|
||||||
|
"usgovarizona",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export interface IGraphConfig {
|
|||||||
|
|
||||||
interface GraphTabOptions extends ViewModels.TabOptions {
|
interface GraphTabOptions extends ViewModels.TabOptions {
|
||||||
account: DatabaseAccount;
|
account: DatabaseAccount;
|
||||||
masterKey: string;
|
password: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
collectionPartitionKeyProperty: string;
|
collectionPartitionKeyProperty: string;
|
||||||
@@ -107,7 +107,7 @@ export default class GraphTab extends TabsBase {
|
|||||||
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
|
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
|
||||||
databaseId: options.databaseId,
|
databaseId: options.databaseId,
|
||||||
collectionId: options.collectionId,
|
collectionId: options.collectionId,
|
||||||
masterKey: options.masterKey,
|
password: options.password,
|
||||||
onLoadStartKey: options.onLoadStartKey,
|
onLoadStartKey: options.onLoadStartKey,
|
||||||
onLoadStartKeyChange: (onLoadStartKey: number): void => {
|
onLoadStartKeyChange: (onLoadStartKey: number): void => {
|
||||||
if (onLoadStartKey === undefined) {
|
if (onLoadStartKey === undefined) {
|
||||||
|
|||||||
78
src/Explorer/Tabs/Shared/CloudShellIPChecker.ts
Normal file
78
src/Explorer/Tabs/Shared/CloudShellIPChecker.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { configContext } from "ConfigContext";
|
||||||
|
import * as DataModels from "Contracts/DataModels";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import { armRequest } from "Utils/arm/request";
|
||||||
|
import {
|
||||||
|
CloudShellIPRange,
|
||||||
|
getCloudShellIPsForRegion,
|
||||||
|
getClusterRegion,
|
||||||
|
} from "../CloudShellTab/Utils/CloudShellIPUtils";
|
||||||
|
import { getNormalizedRegion } from "../CloudShellTab/Utils/RegionUtils";
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const DEFAULT_CLOUDSHELL_REGION = "westus";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has added all CloudShell IPs for their normalized region
|
||||||
|
* @param apiVersion - The API version to use for the ARM request
|
||||||
|
* @returns Promise<boolean> - true if all CloudShell IPs are configured (don't show screenshot), false if missing (show screenshot)
|
||||||
|
*/
|
||||||
|
export async function checkCloudShellIPsConfigured(apiVersion: string): Promise<boolean> {
|
||||||
|
const clusterRegion = getClusterRegion();
|
||||||
|
|
||||||
|
if (!clusterRegion) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedRegion = getNormalizedRegion(clusterRegion, DEFAULT_CLOUDSHELL_REGION);
|
||||||
|
const cloudShellIPs = getCloudShellIPsForRegion(normalizedRegion);
|
||||||
|
|
||||||
|
if (cloudShellIPs.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
|
||||||
|
const response: any = await armRequest({
|
||||||
|
host: configContext.ARM_ENDPOINT,
|
||||||
|
path: firewallRulesUri,
|
||||||
|
method: "GET",
|
||||||
|
apiVersion: apiVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
const firewallRules: DataModels.FirewallRule[] = response?.data?.value || response?.value || [];
|
||||||
|
|
||||||
|
const missingIPs: Array<{ startIP: string; endIP: string; reason?: string }> = [];
|
||||||
|
const foundIPs: Array<{ startIP: string; endIP: string; ruleName?: string }> = [];
|
||||||
|
|
||||||
|
for (const cloudShellIP of cloudShellIPs) {
|
||||||
|
const matchingRule = firewallRules.find((rule) => {
|
||||||
|
const startMatch = rule.properties.startIpAddress === cloudShellIP.startIP;
|
||||||
|
const endMatch = rule.properties.endIpAddress === cloudShellIP.endIP;
|
||||||
|
return startMatch && endMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingRule) {
|
||||||
|
foundIPs.push({ ...cloudShellIP, ruleName: matchingRule.name });
|
||||||
|
} else {
|
||||||
|
missingIPs.push({ ...cloudShellIP, reason: "No exact IP match in firewall rules" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allConfigured = missingIPs.length === 0;
|
||||||
|
return allConfigured;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the normalized region and its CloudShell IPs for display in the guide
|
||||||
|
* @returns Object with region and IPs for the guide
|
||||||
|
*/
|
||||||
|
export function getCloudShellGuideInfo(): { region: string; cloudShellIPs: readonly CloudShellIPRange[] } {
|
||||||
|
const clusterRegion = getClusterRegion();
|
||||||
|
const normalizedRegion = getNormalizedRegion(clusterRegion || "", DEFAULT_CLOUDSHELL_REGION);
|
||||||
|
const cloudShellIPs = getCloudShellIPsForRegion(normalizedRegion);
|
||||||
|
|
||||||
|
return {
|
||||||
|
region: normalizedRegion,
|
||||||
|
cloudShellIPs: cloudShellIPs,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -22,10 +22,22 @@ export abstract class BaseTerminalComponentAdapter implements ReactAdapter {
|
|||||||
protected getUsername: () => string,
|
protected getUsername: () => string,
|
||||||
protected isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
|
protected isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
|
||||||
protected kind: ViewModels.TerminalKind,
|
protected kind: ViewModels.TerminalKind,
|
||||||
) {}
|
protected isCloudShellIPsConfigured?: ko.Observable<boolean>,
|
||||||
|
) { }
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
if (!this.isAllPublicIPAddressesEnabled()) {
|
const publicIPEnabled = this.isAllPublicIPAddressesEnabled();
|
||||||
|
const cloudShellConfigured = this.isCloudShellIPsConfigured ? this.isCloudShellIPsConfigured() : true;
|
||||||
|
let shouldShowScreenshot: boolean;
|
||||||
|
|
||||||
|
if (this.isCloudShellIPsConfigured) {
|
||||||
|
shouldShowScreenshot = !cloudShellConfigured;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
shouldShowScreenshot = !publicIPEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldShowScreenshot) {
|
||||||
return (
|
return (
|
||||||
<QuickstartFirewallNotification
|
<QuickstartFirewallNotification
|
||||||
messageType={this.getMessageType()}
|
messageType={this.getMessageType()}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||||
|
import { checkCloudShellIPsConfigured } from "Explorer/Tabs/Shared/CloudShellIPChecker";
|
||||||
import { CloudShellTerminalComponentAdapter } from "Explorer/Tabs/ShellAdapters/CloudShellTerminalComponentAdapter";
|
import { CloudShellTerminalComponentAdapter } from "Explorer/Tabs/ShellAdapters/CloudShellTerminalComponentAdapter";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
@@ -23,11 +24,13 @@ export default class TerminalTab extends TabsBase {
|
|||||||
private container: Explorer;
|
private container: Explorer;
|
||||||
private notebookTerminalComponentAdapter: ReactAdapter;
|
private notebookTerminalComponentAdapter: ReactAdapter;
|
||||||
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
|
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
|
||||||
|
private isCloudShellIPsConfigured: ko.Observable<boolean>;
|
||||||
|
|
||||||
constructor(options: TerminalTabOptions) {
|
constructor(options: TerminalTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
||||||
|
this.isCloudShellIPsConfigured = ko.observable(true); // Start optimistic, will be updated
|
||||||
|
|
||||||
const commonArgs: [
|
const commonArgs: [
|
||||||
() => DataModels.DatabaseAccount,
|
() => DataModels.DatabaseAccount,
|
||||||
@@ -36,18 +39,33 @@ export default class TerminalTab extends TabsBase {
|
|||||||
ko.Observable<boolean>,
|
ko.Observable<boolean>,
|
||||||
ViewModels.TerminalKind,
|
ViewModels.TerminalKind,
|
||||||
] = [
|
] = [
|
||||||
() => userContext?.databaseAccount,
|
() => userContext?.databaseAccount,
|
||||||
() => this.tabId,
|
() => this.tabId,
|
||||||
() => this.getUsername(),
|
() => this.getUsername(),
|
||||||
this.isAllPublicIPAddressesEnabled,
|
this.isAllPublicIPAddressesEnabled,
|
||||||
options.kind,
|
options.kind,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (userContext.features.enableCloudShell) {
|
if (userContext.features.enableCloudShell) {
|
||||||
this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(...commonArgs);
|
this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(
|
||||||
|
() => userContext?.databaseAccount,
|
||||||
|
() => this.tabId,
|
||||||
|
() => this.getUsername(),
|
||||||
|
this.isAllPublicIPAddressesEnabled,
|
||||||
|
options.kind,
|
||||||
|
this.isCloudShellIPsConfigured,
|
||||||
|
);
|
||||||
|
|
||||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
return this.isTemplateReady() && this.isAllPublicIPAddressesEnabled();
|
const cloudShellConfigured = this.isCloudShellIPsConfigured();
|
||||||
|
return this.isTemplateReady() && cloudShellConfigured;
|
||||||
|
});
|
||||||
|
|
||||||
|
checkCloudShellIPsConfigured("2023-03-01-preview").then(result => {
|
||||||
|
this.isCloudShellIPsConfigured(result);
|
||||||
|
}).catch(error => {
|
||||||
|
console.error(`CloudShell IP Check failed for ${ViewModels.TerminalKind[options.kind]} terminal:`, error);
|
||||||
|
this.isCloudShellIPsConfigured(false);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||||
@@ -65,22 +83,25 @@ export default class TerminalTab extends TabsBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.kind === ViewModels.TerminalKind.Postgres) {
|
// Only run legacy firewall checks for NON-CloudShell terminals cloudShell terminals use the CloudShell IP checker instead
|
||||||
checkFirewallRules(
|
if (!userContext.features.enableCloudShell) {
|
||||||
"2022-11-08",
|
if (options.kind === ViewModels.TerminalKind.Postgres) {
|
||||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
|
checkFirewallRules(
|
||||||
this.isAllPublicIPAddressesEnabled,
|
"2022-11-08",
|
||||||
);
|
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
|
||||||
}
|
this.isAllPublicIPAddressesEnabled,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
|
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
|
||||||
checkFirewallRules(
|
checkFirewallRules(
|
||||||
"2023-03-01-preview",
|
"2023-03-01-preview",
|
||||||
(rule) =>
|
(rule) =>
|
||||||
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
||||||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
|
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
|
||||||
this.isAllPublicIPAddressesEnabled,
|
this.isAllPublicIPAddressesEnabled,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||||
|
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -479,9 +480,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
node: this,
|
node: this,
|
||||||
title: title,
|
title: title,
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
|
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||||
collection: this,
|
collection: this,
|
||||||
masterKey: userContext.masterKey || "",
|
|
||||||
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
||||||
collectionId: this.id(),
|
collectionId: this.id(),
|
||||||
databaseId: this.databaseId,
|
databaseId: this.databaseId,
|
||||||
@@ -737,7 +737,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
title: title,
|
title: title,
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
collection: this,
|
collection: this,
|
||||||
masterKey: userContext.masterKey || "",
|
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||||
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
||||||
collectionId: this.id(),
|
collectionId: this.id(),
|
||||||
databaseId: this.databaseId,
|
databaseId: this.databaseId,
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -23,6 +28,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -45,6 +55,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -65,6 +80,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -123,6 +143,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -138,6 +163,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -187,6 +217,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -257,6 +292,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -323,7 +363,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -354,7 +394,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -386,7 +426,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -422,7 +462,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -490,7 +530,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -521,7 +561,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -580,7 +620,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -666,7 +706,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ const App: React.FunctionComponent = () => {
|
|||||||
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
|
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
|
||||||
useAADAuth();
|
useAADAuth(config);
|
||||||
|
|
||||||
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
||||||
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
||||||
const [connectionString, setConnectionString] = React.useState<string>();
|
const [connectionString, setConnectionString] = React.useState<string>();
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export class PhoenixClient {
|
|||||||
|
|
||||||
private getPhoenixControlPlanePathPrefix(): string {
|
private getPhoenixControlPlanePathPrefix(): string {
|
||||||
if (!this.armResourceId) {
|
if (!this.armResourceId) {
|
||||||
throw new Error("The Phoenix client was not initialized properly: missing ARM resourcce id");
|
throw new Error("The Phoenix client was not initialized properly: missing ARM resource id");
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolsEndpoint =
|
const toolsEndpoint =
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
enableCloudShell: "true" === get("enablecloudshell"),
|
enableCloudShell: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
|||||||
host: configContext.CATALOG_ENDPOINT,
|
host: configContext.CATALOG_ENDPOINT,
|
||||||
path: getOfferingIdPathForRegion(),
|
path: getOfferingIdPathForRegion(),
|
||||||
method: "GET",
|
method: "GET",
|
||||||
apiVersion: "2023-05-01-preview",
|
apiVersion: configContext.CATALOG_API_VERSION,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
filter:
|
filter:
|
||||||
"armRegionName eq '" +
|
"armRegionName eq '" +
|
||||||
|
|||||||
@@ -91,5 +91,5 @@ export const getItemName = (): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isDataplaneRbacSupported = (apiType: string): boolean => {
|
export const isDataplaneRbacSupported = (apiType: string): boolean => {
|
||||||
return apiType === "SQL" || apiType === "Tables";
|
return apiType === "SQL" || apiType === "Tables" || apiType === "Gremlin";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,51 @@
|
|||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { ApiType, updateUserContext, userContext } from "../UserContext";
|
||||||
import * as AuthorizationUtils from "./AuthorizationUtils";
|
import * as AuthorizationUtils from "./AuthorizationUtils";
|
||||||
jest.mock("../Explorer/Explorer");
|
jest.mock("../Explorer/Explorer");
|
||||||
|
|
||||||
describe("AuthorizationUtils", () => {
|
describe("AuthorizationUtils", () => {
|
||||||
|
const setAadDataPlane = (enabled: boolean) => {
|
||||||
|
updateUserContext({
|
||||||
|
features: {
|
||||||
|
enableAadDataPlane: enabled,
|
||||||
|
canExceedMaximumValue: false,
|
||||||
|
cosmosdb: false,
|
||||||
|
enableChangeFeedPolicy: false,
|
||||||
|
enableFixedCollectionWithSharedThroughput: false,
|
||||||
|
enableKOPanel: false,
|
||||||
|
enableNotebooks: false,
|
||||||
|
enableReactPane: false,
|
||||||
|
enableRightPanelV2: false,
|
||||||
|
enableSchema: false,
|
||||||
|
enableSDKoperations: false,
|
||||||
|
enableSpark: false,
|
||||||
|
enableTtl: false,
|
||||||
|
executeSproc: false,
|
||||||
|
enableResourceGraph: false,
|
||||||
|
enableKoResourceTree: false,
|
||||||
|
enableThroughputBuckets: false,
|
||||||
|
hostedDataExplorer: false,
|
||||||
|
sandboxNotebookOutputs: false,
|
||||||
|
showMinRUSurvey: false,
|
||||||
|
ttl90Days: false,
|
||||||
|
enableThroughputCap: false,
|
||||||
|
enableHierarchicalKeys: false,
|
||||||
|
enableCopilot: false,
|
||||||
|
disableCopilotPhoenixGateaway: false,
|
||||||
|
enableCopilotFullSchema: false,
|
||||||
|
copilotChatFixedMonacoEditorHeight: false,
|
||||||
|
enablePriorityBasedExecution: false,
|
||||||
|
disableConnectionStringLogin: false,
|
||||||
|
enableCloudShell: false,
|
||||||
|
autoscaleDefault: false,
|
||||||
|
partitionKeyDefault: false,
|
||||||
|
partitionKeyDefault2: false,
|
||||||
|
notebooksDownBanner: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
describe("getAuthorizationHeader()", () => {
|
describe("getAuthorizationHeader()", () => {
|
||||||
it("should return authorization header if authentication type is AAD", () => {
|
it("should return authorization header if authentication type is AAD", () => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
@@ -54,4 +95,41 @@ describe("AuthorizationUtils", () => {
|
|||||||
).toBeDefined();
|
).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("useDataplaneRbacAuthorization()", () => {
|
||||||
|
it("should return true if enableAadDataPlane feature flag is set", () => {
|
||||||
|
setAadDataPlane(true);
|
||||||
|
expect(AuthorizationUtils.useDataplaneRbacAuthorization(userContext)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true if dataPlaneRbacEnabled is set to true and API supports RBAC", () => {
|
||||||
|
setAadDataPlane(false);
|
||||||
|
["SQL", "Tables", "Gremlin"].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);
|
||||||
|
["Mongo", "Cassandra", "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,6 @@
|
|||||||
import * as msal from "@azure/msal-browser";
|
import * as msal from "@azure/msal-browser";
|
||||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
@@ -7,7 +8,7 @@ import { configContext } from "../ConfigContext";
|
|||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../UserContext";
|
import { UserContext, userContext } from "../UserContext";
|
||||||
|
|
||||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||||
if (userContext.authType === AuthType.EncryptedToken) {
|
if (userContext.authType === AuthType.EncryptedToken) {
|
||||||
@@ -179,3 +180,10 @@ export async function acquireTokenWithMsal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDataplaneRbacAuthorization(userContext: UserContext): boolean {
|
||||||
|
return (
|
||||||
|
userContext.features.enableAadDataPlane ||
|
||||||
|
(userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const autoPilotThroughput1K = 1000;
|
export const autoPilotThroughput1K = 1000;
|
||||||
export const autoPilotIncrementStep = 1000;
|
export const autoPilotIncrementStep = 1000;
|
||||||
export const autoPilotThroughput4K = 4000;
|
export const autoPilotThroughput4K = 4000;
|
||||||
|
export const autoPilotThroughput5K = 5000;
|
||||||
export const autoPilotThroughput10K = 10000;
|
export const autoPilotThroughput10K = 10000;
|
||||||
|
|
||||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
@@ -18,5 +19,8 @@ export const isServerlessAccount = (): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isVectorSearchEnabled = (): boolean => {
|
export const isVectorSearchEnabled = (): boolean => {
|
||||||
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch);
|
return (
|
||||||
|
userContext.apiType === "SQL" &&
|
||||||
|
(isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch) || isFabricNative())
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,32 +45,25 @@ export const defaultAllowedArmEndpoints: ReadonlyArray<string> = [
|
|||||||
"https://management.chinacloudapi.cn",
|
"https://management.chinacloudapi.cn",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const allowedAadEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedAadEndpoints: ReadonlyArray<string> = [
|
||||||
"https://login.microsoftonline.com/",
|
"https://login.microsoftonline.com/",
|
||||||
"https://login.microsoftonline.us/",
|
"https://login.microsoftonline.us/",
|
||||||
"https://login.partner.microsoftonline.cn/",
|
"https://login.partner.microsoftonline.cn/",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const defaultAllowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.microsoft.com"];
|
||||||
|
|
||||||
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
||||||
"https://localhost:12901",
|
|
||||||
"https://localhost:1234",
|
"https://localhost:1234",
|
||||||
|
PortalBackendEndpoints.Development,
|
||||||
|
PortalBackendEndpoints.Mpac,
|
||||||
|
PortalBackendEndpoints.Prod,
|
||||||
|
PortalBackendEndpoints.Fairfax,
|
||||||
|
PortalBackendEndpoints.Mooncake,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const PortalBackendOutboundIPs: { [key: string]: string[] } = {
|
|
||||||
[PortalBackendEndpoints.Mpac]: ["13.91.105.215", "4.210.172.107"],
|
|
||||||
[PortalBackendEndpoints.Prod]: ["13.88.56.148", "40.91.218.243"],
|
|
||||||
[PortalBackendEndpoints.Fairfax]: ["52.247.163.6", "52.244.134.181"],
|
|
||||||
[PortalBackendEndpoints.Mooncake]: ["163.228.137.6", "143.64.170.142"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
|
||||||
[MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"],
|
|
||||||
[MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"],
|
|
||||||
[MongoProxyEndpoints.Fairfax]: ["52.244.176.112", "52.247.148.42"],
|
|
||||||
[MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||||
|
"https://localhost:1234",
|
||||||
MongoProxyEndpoints.Development,
|
MongoProxyEndpoints.Development,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
@@ -86,19 +79,8 @@ export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
|||||||
CassandraProxyEndpoints.Mooncake,
|
CassandraProxyEndpoints.Mooncake,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CassandraProxyOutboundIPs: { [key: string]: string[] } = {
|
|
||||||
[CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"],
|
|
||||||
[CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"],
|
|
||||||
[CassandraProxyEndpoints.Fairfax]: ["52.244.50.101", "52.227.165.24"],
|
|
||||||
[CassandraProxyEndpoints.Mooncake]: ["40.73.99.146", "143.64.62.47"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
||||||
|
|
||||||
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
|
|
||||||
|
|
||||||
export const allowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.microsoft.com"];
|
|
||||||
|
|
||||||
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];
|
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];
|
||||||
|
|
||||||
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
|
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import * as msal from "@azure/msal-browser";
|
import * as msal from "@azure/msal-browser";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { configContext } from "../ConfigContext";
|
import { ConfigContext } from "../ConfigContext";
|
||||||
import { acquireTokenWithMsal, getMsalInstance } from "../Utils/AuthorizationUtils";
|
import { acquireTokenWithMsal, getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||||
|
|
||||||
const msalInstance = await getMsalInstance();
|
|
||||||
|
|
||||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
|
||||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||||
|
|
||||||
interface ReturnType {
|
interface ReturnType {
|
||||||
@@ -27,57 +24,97 @@ export interface AadAuthFailure {
|
|||||||
failureLinkAction?: () => void;
|
failureLinkAction?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAADAuth(): ReturnType {
|
export function useAADAuth(config?: ConfigContext): ReturnType {
|
||||||
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(
|
const [msalInstance, setMsalInstance] = React.useState<msal.PublicClientApplication | null>(null);
|
||||||
Boolean(cachedAccount && cachedTenantId) || false,
|
const [isLoggedIn, { setTrue: setLoggedIn, setFalse: setLoggedOut }] = useBoolean(false);
|
||||||
);
|
const [account, setAccount] = React.useState<msal.AccountInfo>(null);
|
||||||
const [account, setAccount] = React.useState<msal.AccountInfo>(cachedAccount);
|
|
||||||
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
|
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
|
||||||
const [graphToken, setGraphToken] = React.useState<string>();
|
const [graphToken, setGraphToken] = React.useState<string>();
|
||||||
const [armToken, setArmToken] = React.useState<string>();
|
const [armToken, setArmToken] = React.useState<string>();
|
||||||
const [authFailure, setAuthFailure] = React.useState<AadAuthFailure>(undefined);
|
const [authFailure, setAuthFailure] = React.useState<AadAuthFailure>(undefined);
|
||||||
|
|
||||||
msalInstance.setActiveAccount(account);
|
// Initialize MSAL instance when config is available
|
||||||
const login = React.useCallback(async () => {
|
React.useEffect(() => {
|
||||||
const response = await msalInstance.loginPopup({
|
if (config && !msalInstance) {
|
||||||
redirectUri: configContext.msalRedirectURI,
|
getMsalInstance().then((instance) => {
|
||||||
scopes: [],
|
setMsalInstance(instance);
|
||||||
});
|
const cachedAccount = instance.getAllAccounts()?.[0];
|
||||||
setLoggedIn();
|
if (cachedAccount && cachedTenantId) {
|
||||||
setAccount(response.account);
|
setAccount(cachedAccount);
|
||||||
setTenantId(response.tenantId);
|
setLoggedIn();
|
||||||
localStorage.setItem("cachedTenantId", response.tenantId);
|
instance.setActiveAccount(cachedAccount);
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
const logout = React.useCallback(() => {
|
|
||||||
setLoggedOut();
|
|
||||||
localStorage.removeItem("cachedTenantId");
|
|
||||||
msalInstance.logoutRedirect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const switchTenant = React.useCallback(
|
|
||||||
async (id) => {
|
|
||||||
const response = await msalInstance.loginPopup({
|
|
||||||
redirectUri: configContext.msalRedirectURI,
|
|
||||||
authority: `${configContext.AAD_ENDPOINT}${id}`,
|
|
||||||
scopes: [],
|
|
||||||
});
|
});
|
||||||
setTenantId(response.tenantId);
|
}
|
||||||
setAccount(response.account);
|
}, [config, msalInstance]);
|
||||||
localStorage.setItem("cachedTenantId", response.tenantId);
|
|
||||||
},
|
|
||||||
[account, tenantId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const acquireTokens = React.useCallback(async () => {
|
React.useEffect(() => {
|
||||||
if (!(account && tenantId)) {
|
if (msalInstance && account) {
|
||||||
|
msalInstance.setActiveAccount(account);
|
||||||
|
}
|
||||||
|
}, [msalInstance, account]);
|
||||||
|
|
||||||
|
const login = React.useCallback(async () => {
|
||||||
|
if (!msalInstance || !config) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await msalInstance.loginPopup({
|
||||||
|
redirectUri: config.msalRedirectURI,
|
||||||
|
scopes: [],
|
||||||
|
});
|
||||||
|
setLoggedIn();
|
||||||
|
setAccount(response.account);
|
||||||
|
setTenantId(response.tenantId);
|
||||||
|
localStorage.setItem("cachedTenantId", response.tenantId);
|
||||||
|
} catch (error) {
|
||||||
|
setAuthFailure({
|
||||||
|
failureMessage: `Login failed: ${JSON.stringify(error)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [msalInstance, config]);
|
||||||
|
|
||||||
|
const logout = React.useCallback(() => {
|
||||||
|
if (!msalInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoggedOut();
|
||||||
|
localStorage.removeItem("cachedTenantId");
|
||||||
|
msalInstance.logoutRedirect();
|
||||||
|
}, [msalInstance]);
|
||||||
|
|
||||||
|
const switchTenant = React.useCallback(
|
||||||
|
async (id) => {
|
||||||
|
if (!msalInstance || !config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await msalInstance.loginPopup({
|
||||||
|
redirectUri: config.msalRedirectURI,
|
||||||
|
authority: `${config.AAD_ENDPOINT}${id}`,
|
||||||
|
scopes: [],
|
||||||
|
});
|
||||||
|
setTenantId(response.tenantId);
|
||||||
|
setAccount(response.account);
|
||||||
|
localStorage.setItem("cachedTenantId", response.tenantId);
|
||||||
|
} catch (error) {
|
||||||
|
setAuthFailure({
|
||||||
|
failureMessage: `Tenant switch failed: ${JSON.stringify(error)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[msalInstance, config],
|
||||||
|
);
|
||||||
|
|
||||||
|
const acquireTokens = React.useCallback(async () => {
|
||||||
|
if (!(account && tenantId && msalInstance && config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const armToken = await acquireTokenWithMsal(msalInstance, {
|
const armToken = await acquireTokenWithMsal(msalInstance, {
|
||||||
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
authority: `${config.AAD_ENDPOINT}${tenantId}`,
|
||||||
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
scopes: [`${config.ARM_ENDPOINT}/.default`],
|
||||||
});
|
});
|
||||||
|
|
||||||
setArmToken(armToken);
|
setArmToken(armToken);
|
||||||
@@ -105,8 +142,8 @@ export function useAADAuth(): ReturnType {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const graphToken = await acquireTokenWithMsal(msalInstance, {
|
const graphToken = await acquireTokenWithMsal(msalInstance, {
|
||||||
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
authority: `${config.AAD_ENDPOINT}${tenantId}`,
|
||||||
scopes: [`${configContext.GRAPH_ENDPOINT}/.default`],
|
scopes: [`${config.GRAPH_ENDPOINT}/.default`],
|
||||||
});
|
});
|
||||||
|
|
||||||
setGraphToken(graphToken);
|
setGraphToken(graphToken);
|
||||||
@@ -115,7 +152,7 @@ export function useAADAuth(): ReturnType {
|
|||||||
// it's not critical if this fails.
|
// it's not critical if this fails.
|
||||||
console.warn("Error acquiring graph token: " + error);
|
console.warn("Error acquiring graph token: " + error);
|
||||||
}
|
}
|
||||||
}, [account, tenantId]);
|
}, [account, tenantId, msalInstance, config]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (account && tenantId && !authFailure) {
|
if (account && tenantId && !authFailure) {
|
||||||
|
|||||||
@@ -894,6 +894,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
|
|
||||||
const authorizationToken = inputs.authorizationToken || "";
|
const authorizationToken = inputs.authorizationToken || "";
|
||||||
const databaseAccount = inputs.databaseAccount;
|
const databaseAccount = inputs.databaseAccount;
|
||||||
|
const aadToken = inputs.aadToken || "";
|
||||||
|
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||||
@@ -906,6 +907,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
authorizationToken,
|
authorizationToken,
|
||||||
|
aadToken,
|
||||||
databaseAccount,
|
databaseAccount,
|
||||||
resourceGroup: inputs.resourceGroup,
|
resourceGroup: inputs.resourceGroup,
|
||||||
subscriptionId: inputs.subscriptionId,
|
subscriptionId: inputs.subscriptionId,
|
||||||
|
|||||||
@@ -47,3 +47,6 @@ require("jquery-ui-dist/jquery-ui");
|
|||||||
unobserve: jest.fn(),
|
unobserve: jest.fn(),
|
||||||
disconnect: jest.fn(),
|
disconnect: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// The test environment Data Explorer uses does not have crypto.subtle implementation
|
||||||
|
(<any>global).crypto.subtle = {};
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ The tests run in [Playwright](https://playwright.dev/), using the official Playw
|
|||||||
|
|
||||||
To run all the tests, you need:
|
To run all the tests, you need:
|
||||||
|
|
||||||
* A CosmosDB Account using the Cassandra API
|
- A CosmosDB Account using the Cassandra API
|
||||||
* A CosmosDB Account using the Gremlin API
|
- A CosmosDB Account using the Gremlin API
|
||||||
* A CosmosDB Account using the MongoDB API, API version 6.0
|
- A CosmosDB Account using the MongoDB API, API version 6.0
|
||||||
* A CosmosDB Account using the MongoDB API, API version 3.2
|
- A CosmosDB Account using the MongoDB API, API version 3.2
|
||||||
* A CosmosDB Account using the NoSQL API
|
- A CosmosDB Account using the NoSQL API
|
||||||
* A CosmosDB Account using the Tables API
|
- A CosmosDB Account using the Tables API
|
||||||
|
|
||||||
Each Account must have at least 1000 RU/s of throughput available for new databases/collections/etc.
|
Each Account must have at least 1000 RU/s of throughput available for new databases/collections/etc.
|
||||||
The tests create new databases/keyspaces/etc. for each test, and delete them when the test is done.
|
The tests create new databases/keyspaces/etc. for each test, and delete them when the test is done.
|
||||||
@@ -62,10 +62,10 @@ Do you want to continue? (y/n):
|
|||||||
|
|
||||||
This prompt shows:
|
This prompt shows:
|
||||||
|
|
||||||
* The resources that will be deployed, in this case, all of them. You can filter to deploy only a subset by specifying the `-ResourceTypes` parameter. For example `-ResourceTypes @("cassandra", "sql")`.
|
- The resources that will be deployed, in this case, all of them. You can filter to deploy only a subset by specifying the `-ResourceTypes` parameter. For example `-ResourceTypes @("cassandra", "sql")`.
|
||||||
* The location the resources will be deployed to, `West US 3` in this case.
|
- The location the resources will be deployed to, `West US 3` in this case.
|
||||||
* The resource group that will be used, `ashleyst-e2e-testing` in this case.
|
- The resource group that will be used, `ashleyst-e2e-testing` in this case.
|
||||||
* The subscription that will be used.
|
- The subscription that will be used.
|
||||||
|
|
||||||
Once you confirm, the resources will be deployed using Azure PowerShell and the Bicep templates in the `resources` directory. The script will wait for all the deployments to complete before exiting.
|
Once you confirm, the resources will be deployed using Azure PowerShell and the Bicep templates in the `resources` directory. The script will wait for all the deployments to complete before exiting.
|
||||||
|
|
||||||
@@ -76,18 +76,18 @@ You can re-run this script at any time to update the resources, if the Bicep tem
|
|||||||
Before running the tests, you need to configure your environment to specify the accounts to use for testing.
|
Before running the tests, you need to configure your environment to specify the accounts to use for testing.
|
||||||
The following environment variables are used:
|
The following environment variables are used:
|
||||||
|
|
||||||
* `DE_TEST_RESOURCE_GROUP` - The resource group to use for testing. This should be the same resource group that the resources were deployed to.
|
- `DE_TEST_RESOURCE_GROUP` - The resource group to use for testing. This should be the same resource group that the resources were deployed to.
|
||||||
* `DE_TEST_SUBSCRIPTION_ID` - The subscription ID to use for testing. This should be the same subscription that the resources were deployed to.
|
- `DE_TEST_SUBSCRIPTION_ID` - The subscription ID to use for testing. This should be the same subscription that the resources were deployed to.
|
||||||
* `DE_TEST_ACCOUNT_PREFIX` - If you used the default naming scheme provided by the `deploy.ps1` script, this should be your Windows username (or whatever value you passed in for the `-ResourcePrefix` argument when deploying). This is used to find the accounts that were deployed.
|
- `DE_TEST_ACCOUNT_PREFIX` - If you used the default naming scheme provided by the `deploy.ps1` script, this should be your Windows username (or whatever value you passed in for the `-ResourcePrefix` argument when deploying). This is used to find the accounts that were deployed.
|
||||||
|
|
||||||
In the event you didn't use the `deploy.ps1` script, you can specify the accounts directly using the following environment variables:
|
In the event you didn't use the `deploy.ps1` script, you can specify the accounts directly using the following environment variables:
|
||||||
|
|
||||||
* `DE_TEST_ACCOUNT_NAME_CASSANDRA` - The name of the CosmosDB Account using the Cassandra API.
|
- `DE_TEST_ACCOUNT_NAME_CASSANDRA` - The name of the CosmosDB Account using the Cassandra API.
|
||||||
* `DE_TEST_ACCOUNT_NAME_GREMLIN` - The name of the CosmosDB Account using the Gremlin API.
|
- `DE_TEST_ACCOUNT_NAME_GREMLIN` - The name of the CosmosDB Account using the Gremlin API.
|
||||||
* `DE_TEST_ACCOUNT_NAME_MONGO` - The name of the CosmosDB Account using the MongoDB API, API version 6.0.
|
- `DE_TEST_ACCOUNT_NAME_MONGO` - The name of the CosmosDB Account using the MongoDB API, API version 6.0.
|
||||||
* `DE_TEST_ACCOUNT_NAME_MONGO32` - The name of the CosmosDB Account using the MongoDB API, API version 3.2.
|
- `DE_TEST_ACCOUNT_NAME_MONGO32` - The name of the CosmosDB Account using the MongoDB API, API version 3.2.
|
||||||
* `DE_TEST_ACCOUNT_NAME_SQL` - The name of the CosmosDB Account using the NoSQL API.
|
- `DE_TEST_ACCOUNT_NAME_SQL` - The name of the CosmosDB Account using the NoSQL API.
|
||||||
* `DE_TEST_ACCOUNT_NAME_TABLES` - The name of the CosmosDB Account using the Tables API.
|
- `DE_TEST_ACCOUNT_NAME_TABLES` - The name of the CosmosDB Account using the Tables API.
|
||||||
|
|
||||||
If you used all the standard deployment scripts and naming scheme, you can set these environment variables using the following command:
|
If you used all the standard deployment scripts and naming scheme, you can set these environment variables using the following command:
|
||||||
|
|
||||||
@@ -152,6 +152,46 @@ The UI allows you to select a specific test to run and to see the results of the
|
|||||||
|
|
||||||
See the [Playwright docs](https://playwright.dev/docs/running-tests) for more information on running tests.
|
See the [Playwright docs](https://playwright.dev/docs/running-tests) for more information on running tests.
|
||||||
|
|
||||||
|
### Testing with Data Plane RBAC Authentication
|
||||||
|
|
||||||
|
By default, the tests will use key based authentication to access the database accounts. For APIs that support data plane RBAC, the
|
||||||
|
test can be configured to use that instead, by acquiring access tokens and setting them to environment variables:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# NoSQL API
|
||||||
|
$ENV:NOSQL_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
|
# NoSQL API (Readonly)
|
||||||
|
$ENV:NOSQL_READONLY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
|
# Tables API
|
||||||
|
$ENV:TABLE_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
|
||||||
|
# Gremlin API
|
||||||
|
$ENV:GREMLIN_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
|
||||||
|
```
|
||||||
|
|
||||||
|
When setting up test accounts to use dataplane RBAC, you will need to create custom role definitions with the following roles:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# NoSQL API roles
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/readMetadata
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/sqlDatabases/*
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/throughputSettings/*
|
||||||
|
|
||||||
|
# Tables API roles
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/readMetadata
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/tables/*
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/throughputSettings/*
|
||||||
|
|
||||||
|
# Gremlin API roles
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/readMetadata
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/gremlin/*
|
||||||
|
Microsoft.DocumentDB/databaseAccounts/throughputSettings/
|
||||||
|
```
|
||||||
|
|
||||||
## Clean-up
|
## Clean-up
|
||||||
|
|
||||||
Tests should clean-up after themselves if they succeed (and sometimes even when they fail).
|
Tests should clean-up after themselves if they succeed (and sometimes even when they fail).
|
||||||
|
|||||||
24
test/fx.ts
24
test/fx.ts
@@ -86,6 +86,30 @@ export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: s
|
|||||||
// For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false.
|
// For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false.
|
||||||
params.set("feature.enableCopilot", "false");
|
params.set("feature.enableCopilot", "false");
|
||||||
|
|
||||||
|
const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
|
||||||
|
if (nosqlRbacToken) {
|
||||||
|
params.set("nosqlRbacToken", nosqlRbacToken);
|
||||||
|
params.set("enableaaddataplane", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN;
|
||||||
|
if (nosqlReadOnlyRbacToken) {
|
||||||
|
params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken);
|
||||||
|
params.set("enableaaddataplane", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN;
|
||||||
|
if (tableRbacToken) {
|
||||||
|
params.set("tableRbacToken", tableRbacToken);
|
||||||
|
params.set("enableaaddataplane", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN;
|
||||||
|
if (gremlinRbacToken) {
|
||||||
|
params.set("gremlinRbacToken", gremlinRbacToken);
|
||||||
|
params.set("enableaaddataplane", "true");
|
||||||
|
}
|
||||||
|
|
||||||
if (iframeSrc) {
|
if (iframeSrc) {
|
||||||
params.set("iframeSrc", iframeSrc);
|
params.set("iframeSrc", iframeSrc);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";
|
|||||||
|
|
||||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||||
import { CosmosClient, PermissionMode } from "@azure/cosmos";
|
import { CosmosClient, PermissionMode } from "@azure/cosmos";
|
||||||
|
import { AzureIdentityCredentialAdapter } from "@azure/ms-rest-js";
|
||||||
import {
|
import {
|
||||||
DataExplorer,
|
DataExplorer,
|
||||||
TestAccount,
|
TestAccount,
|
||||||
@@ -13,8 +14,12 @@ import {
|
|||||||
} from "../fx";
|
} from "../fx";
|
||||||
|
|
||||||
test("SQL account using Resource token", async ({ page }) => {
|
test("SQL account using Resource token", async ({ page }) => {
|
||||||
|
const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN || "";
|
||||||
|
test.skip(nosqlAccountRbacToken.length > 0, "Resource tokens not supported when using data plane RBAC.");
|
||||||
|
|
||||||
const credentials = getAzureCLICredentials();
|
const credentials = getAzureCLICredentials();
|
||||||
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
|
||||||
|
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
|
||||||
const accountName = getAccountName(TestAccount.SQL);
|
const accountName = getAccountName(TestAccount.SQL);
|
||||||
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
||||||
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
|
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||||
import { BulkOperationType, Container, CosmosClient, Database, JSONObject } from "@azure/cosmos";
|
import { BulkOperationType, Container, CosmosClient, CosmosClientOptions, Database, JSONObject } from "@azure/cosmos";
|
||||||
import { AzureIdentityCredentialAdapter } from "@azure/ms-rest-js";
|
import { AzureIdentityCredentialAdapter } from "@azure/ms-rest-js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -82,11 +82,24 @@ export async function createTestSQLContainer(includeTestData?: boolean) {
|
|||||||
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
|
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
|
||||||
const accountName = getAccountName(TestAccount.SQL);
|
const accountName = getAccountName(TestAccount.SQL);
|
||||||
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
||||||
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
|
|
||||||
const client = new CosmosClient({
|
const clientOptions: CosmosClientOptions = {
|
||||||
endpoint: account.documentEndpoint!,
|
endpoint: account.documentEndpoint!,
|
||||||
key: keys.primaryMasterKey,
|
};
|
||||||
});
|
|
||||||
|
const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
|
||||||
|
if (nosqlAccountRbacToken) {
|
||||||
|
clientOptions.tokenProvider = async (): Promise<string> => {
|
||||||
|
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
||||||
|
const authorizationToken = `${AUTH_PREFIX}${nosqlAccountRbacToken}`;
|
||||||
|
return authorizationToken;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
|
||||||
|
clientOptions.key = keys.primaryMasterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new CosmosClient(clientOptions);
|
||||||
const { database } = await client.databases.createIfNotExists({ id: databaseId });
|
const { database } = await client.databases.createIfNotExists({ id: databaseId });
|
||||||
try {
|
try {
|
||||||
const { container } = await database.containers.createIfNotExists({
|
const { container } = await database.containers.createIfNotExists({
|
||||||
|
|||||||
@@ -10,17 +10,45 @@ const subscriptionId = urlSearchParams.get("subscriptionId") || process.env.SUBS
|
|||||||
const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us";
|
const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us";
|
||||||
const selfServeType = urlSearchParams.get("selfServeType") || "example";
|
const selfServeType = urlSearchParams.get("selfServeType") || "example";
|
||||||
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
|
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
|
||||||
const token = urlSearchParams.get("token");
|
const authToken = urlSearchParams.get("token");
|
||||||
|
|
||||||
console.log("Resource Group:", resourceGroup);
|
const nosqlRbacToken = urlSearchParams.get("nosqlRbacToken") || process.env.NOSQL_TESTACCOUNT_TOKEN || "";
|
||||||
console.log("Subcription: ", subscriptionId);
|
const nosqlReadOnlyRbacToken =
|
||||||
console.log("Account Name: ", accountName);
|
urlSearchParams.get("nosqlReadOnlyRbacToken") || process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN || "";
|
||||||
|
const tableRbacToken = urlSearchParams.get("tableRbacToken") || process.env.TABLE_TESTACCOUNT_TOKEN || "";
|
||||||
|
const gremlinRbacToken = urlSearchParams.get("gremlinRbacToken") || process.env.GREMLIN_TESTACCOUNT_TOKEN || "";
|
||||||
|
|
||||||
const initTestExplorer = async (): Promise<void> => {
|
const initTestExplorer = async (): Promise<void> => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
authorizationToken: `bearer ${token}`,
|
authorizationToken: `bearer ${authToken}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const databaseAccount = await get(subscriptionId, resourceGroup, accountName);
|
const databaseAccount = await get(subscriptionId, resourceGroup, accountName);
|
||||||
|
const tags = databaseAccount?.tags;
|
||||||
|
const testAccountType = tags && tags["DataExplorer:TestAccountType"];
|
||||||
|
|
||||||
|
let rbacToken = "";
|
||||||
|
switch (testAccountType) {
|
||||||
|
case "sql":
|
||||||
|
rbacToken = nosqlRbacToken;
|
||||||
|
break;
|
||||||
|
case "sql-readonly":
|
||||||
|
rbacToken = nosqlReadOnlyRbacToken;
|
||||||
|
break;
|
||||||
|
case "gremlin":
|
||||||
|
rbacToken = gremlinRbacToken;
|
||||||
|
break;
|
||||||
|
case "tables":
|
||||||
|
rbacToken = tableRbacToken;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rbacToken.length > 0) {
|
||||||
|
updateUserContext({
|
||||||
|
dataPlaneRbacEnabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const keys = await listKeys(subscriptionId, resourceGroup, accountName);
|
const keys = await listKeys(subscriptionId, resourceGroup, accountName);
|
||||||
|
|
||||||
// Disable the quickstart carousel.
|
// Disable the quickstart carousel.
|
||||||
@@ -33,7 +61,8 @@ const initTestExplorer = async (): Promise<void> => {
|
|||||||
databaseAccount: databaseAccount,
|
databaseAccount: databaseAccount,
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
resourceGroup,
|
resourceGroup,
|
||||||
authorizationToken: `Bearer ${token}`,
|
authorizationToken: `Bearer ${authToken}`,
|
||||||
|
aadToken: rbacToken,
|
||||||
features: {},
|
features: {},
|
||||||
hasWriteAccess: true,
|
hasWriteAccess: true,
|
||||||
csmEndpoint: "https://management.azure.com",
|
csmEndpoint: "https://management.azure.com",
|
||||||
@@ -89,7 +118,7 @@ const initTestExplorer = async (): Promise<void> => {
|
|||||||
iframe.setAttribute("data-test", "DataExplorerFrame");
|
iframe.setAttribute("data-test", "DataExplorerFrame");
|
||||||
iframe.classList.add("iframe");
|
iframe.classList.add("iframe");
|
||||||
iframe.title = "explorer";
|
iframe.title = "explorer";
|
||||||
iframe.src = iframeSrc;
|
iframe.src = iframeSrc; // CodeQL [SM03712] Not used in production, only for testing purposes
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -106,23 +106,21 @@ module.exports = function (_env = {}, argv = {}) {
|
|||||||
typescriptRule.use[0].options.compilerOptions = { target: "ES2018" };
|
typescriptRule.use[0].options.compilerOptions = { target: "ES2018" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugins = [
|
const entry = {
|
||||||
new CleanWebpackPlugin(),
|
main: "./src/Main.tsx",
|
||||||
new webpack.ProvidePlugin({
|
index: "./src/Index.tsx",
|
||||||
process: "process/browser",
|
quickstart: "./src/quickstart.ts",
|
||||||
Buffer: ["buffer", "Buffer"],
|
hostedExplorer: "./src/HostedExplorer.tsx",
|
||||||
}),
|
terminal: "./src/Terminal/index.ts",
|
||||||
new CreateFileWebpack({
|
cellOutputViewer: "./src/CellOutputViewer/CellOutputViewer.tsx",
|
||||||
path: "./dist",
|
notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx",
|
||||||
fileName: "version.txt",
|
galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx",
|
||||||
content: `${gitSha.trim()} ${new Date().toUTCString()}`,
|
selfServe: "./src/SelfServe/SelfServe.tsx",
|
||||||
}),
|
connectToGitHub: "./src/GitHub/GitHubConnector.ts",
|
||||||
// TODO Enable when @nteract once removed
|
...(mode !== "production" && { testExplorer: "./test/testExplorer/TestExplorer.ts" }),
|
||||||
// ./node_modules/@nteract/markdown/node_modules/@nteract/presentational-components/lib/index.js line 63 breaks this with physical file Icon.js referred to as icon.js
|
};
|
||||||
// new CaseSensitivePathsPlugin(),
|
|
||||||
new MiniCssExtractPlugin({
|
const htmlWebpackPlugins = [
|
||||||
filename: "[name].[contenthash].css",
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: "explorer.html",
|
filename: "explorer.html",
|
||||||
template: "src/explorer.html",
|
template: "src/explorer.html",
|
||||||
@@ -148,16 +146,6 @@ module.exports = function (_env = {}, argv = {}) {
|
|||||||
template: "src/hostedExplorer.html",
|
template: "src/hostedExplorer.html",
|
||||||
chunks: ["hostedExplorer"],
|
chunks: ["hostedExplorer"],
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: "testExplorer.html",
|
|
||||||
template: "test/testExplorer/testExplorer.html",
|
|
||||||
chunks: ["testExplorer"],
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: "Heatmap.html",
|
|
||||||
template: "src/Controls/Heatmap/Heatmap.html",
|
|
||||||
chunks: ["heatmap"],
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: "cellOutputViewer.html",
|
filename: "cellOutputViewer.html",
|
||||||
template: "src/CellOutputViewer/cellOutputViewer.html",
|
template: "src/CellOutputViewer/cellOutputViewer.html",
|
||||||
@@ -183,6 +171,35 @@ module.exports = function (_env = {}, argv = {}) {
|
|||||||
template: "src/SelfServe/selfServe.html",
|
template: "src/SelfServe/selfServe.html",
|
||||||
chunks: ["selfServe"],
|
chunks: ["selfServe"],
|
||||||
}),
|
}),
|
||||||
|
...(mode !== "production"
|
||||||
|
? [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: "testExplorer.html",
|
||||||
|
template: "test/testExplorer/testExplorer.html",
|
||||||
|
chunks: ["testExplorer"],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
new CleanWebpackPlugin(),
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
process: "process/browser",
|
||||||
|
Buffer: ["buffer", "Buffer"],
|
||||||
|
}),
|
||||||
|
new CreateFileWebpack({
|
||||||
|
path: "./dist",
|
||||||
|
fileName: "version.txt",
|
||||||
|
content: `${gitSha.trim()} ${new Date().toUTCString()}`,
|
||||||
|
}),
|
||||||
|
// TODO Enable when @nteract once removed
|
||||||
|
// ./node_modules/@nteract/markdown/node_modules/@nteract/presentational-components/lib/index.js line 63 breaks this with physical file Icon.js referred to as icon.js
|
||||||
|
// new CaseSensitivePathsPlugin(),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: "[name].[contenthash].css",
|
||||||
|
}),
|
||||||
|
...htmlWebpackPlugins,
|
||||||
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/cellOutputViewer/]),
|
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/cellOutputViewer/]),
|
||||||
new HTMLInlineCSSWebpackPlugin({
|
new HTMLInlineCSSWebpackPlugin({
|
||||||
filter: (fileName) => fileName.includes("cellOutputViewer"),
|
filter: (fileName) => fileName.includes("cellOutputViewer"),
|
||||||
@@ -205,20 +222,7 @@ module.exports = function (_env = {}, argv = {}) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
mode: mode,
|
mode: mode,
|
||||||
entry: {
|
entry: entry,
|
||||||
main: "./src/Main.tsx",
|
|
||||||
index: "./src/Index.tsx",
|
|
||||||
quickstart: "./src/quickstart.ts",
|
|
||||||
hostedExplorer: "./src/HostedExplorer.tsx",
|
|
||||||
testExplorer: "./test/testExplorer/TestExplorer.ts",
|
|
||||||
heatmap: "./src/Controls/Heatmap/Heatmap.ts",
|
|
||||||
terminal: "./src/Terminal/index.ts",
|
|
||||||
cellOutputViewer: "./src/CellOutputViewer/CellOutputViewer.tsx",
|
|
||||||
notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx",
|
|
||||||
galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx",
|
|
||||||
selfServe: "./src/SelfServe/SelfServe.tsx",
|
|
||||||
connectToGitHub: "./src/GitHub/GitHubConnector.ts",
|
|
||||||
},
|
|
||||||
output: {
|
output: {
|
||||||
chunkFilename: "[name].[chunkhash:6].js",
|
chunkFilename: "[name].[chunkhash:6].js",
|
||||||
filename: "[name].[chunkhash:6].js",
|
filename: "[name].[chunkhash:6].js",
|
||||||
|
|||||||
Reference in New Issue
Block a user