Compare commits

..

48 Commits

Author SHA1 Message Date
Senthamil Sindhu
66a1ca321d Add changes for ARM/AAD Token renewal 2024-11-21 16:01:49 -08:00
Senthamil Sindhu
d12f10afd8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-11-19 11:50:50 -08:00
Senthamil Sindhu
b456e53b2f Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-10-10 00:16:31 -07:00
Senthamil Sindhu
de5ba041e9 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-10-08 14:40:04 -07:00
Senthamil Sindhu
ae7184f7ea Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-10-04 15:50:19 -07:00
Senthamil Sindhu
3a6769280b t Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-10-02 19:56:51 -07:00
Senthamil Sindhu
4768ba3642 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-10-01 19:55:09 -07:00
Senthamil Sindhu
bd564c665b Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-09-17 16:12:01 -07:00
Senthamil Sindhu
7e5c6420ad Run npm format 2024-08-21 13:36:46 -07:00
Senthamil Sindhu
89a3a040d8 Add AAD endpoints 2024-08-21 13:31:02 -07:00
Senthamil Sindhu
ec3afa0526 Add AAD endpoints for all environments 2024-08-21 10:11:35 -07:00
Senthamil Sindhu
4176a8a9a9 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-08-21 10:11:04 -07:00
Senthamil Sindhu
bc8094f44f Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-08-05 10:02:29 -07:00
Senthamil Sindhu
d7825f4f78 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-30 08:23:50 -07:00
Senthamil Sindhu
e51c28c634 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-24 12:32:54 -07:00
Senthamil Sindhu
2b84af60f4 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-19 07:31:11 -07:00
Senthamil Sindhu
29a1a819c3 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-16 14:28:50 -07:00
Senthamil Sindhu
5a16eec29d Add more logs for RBAC features 2024-07-10 07:38:37 -07:00
Senthamil Sindhu
2b11e0e52b Add more logs for RBAC feature 2024-07-10 07:30:34 -07:00
Senthamil Sindhu
89374a16ba Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-09 21:13:41 -07:00
Senthamil Sindhu
dc289ece75 Address feedback 2024-07-09 07:31:09 -07:00
Senthamil Sindhu
1ddd372c6d Add code to fix VCoreMongo/PG bug 2024-07-08 14:46:11 -07:00
Senthamil Sindhu
2740657b4a Remove unnecessary code 2024-07-08 14:39:49 -07:00
Senthamil Sindhu
8c888a751c Remove unnecessary code 2024-07-08 14:26:04 -07:00
Senthamil Sindhu
8140f0edb1 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-08 13:45:54 -07:00
Senthamil Sindhu
ab5239df09 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-08 13:27:24 -07:00
Senthamil Sindhu
3e48393fbb Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-03 00:07:17 -07:00
Senthamil Sindhu
0079a9147f Resolved merge conflict 2024-07-01 16:22:04 -07:00
Senthamil Sindhu
912688dc14 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-06-27 11:00:31 -07:00
Senthamil Sindhu
8849526fab Merge branch 'add-dp-rbac' of https://github.com/Azure/cosmos-explorer 2024-06-19 15:20:20 -07:00
Senthamil Sindhu
24af64a66d Add additional changes for Portal RBAC functionality 2024-06-19 15:05:14 -07:00
Senthamil Sindhu
be871737ad Support data plane RBAC 2024-06-14 12:45:21 -07:00
Senthamil Sindhu
4d8bb5c3ea Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-06-14 12:18:14 -07:00
Senthamil Sindhu
10a8505b9a Support data plane RBAC 2024-06-14 12:12:30 -07:00
Senthamil Sindhu
ef7c2fe2f7 Remove dev endpoint 2024-04-10 11:59:57 -07:00
Senthamil Sindhu
4c7aca95e1 Merge branch 'users/aisayas/mp-cp-activate-prod' of https://github.com/Azure/cosmos-explorer into users/sindhuba/activate-prod 2024-04-09 12:27:51 -07:00
Senthamil Sindhu
2243ad895a Remove prod endpoint 2024-04-09 12:16:13 -07:00
Senthamil Sindhu
b2d5f91fe1 Remove prod 2024-04-09 11:22:17 -07:00
Asier Isayas
a712193477 fix pr check tests 2024-04-09 11:43:24 -04:00
Senthamil Sindhu
5ee411693c Add prod endpoint 2024-04-09 08:41:47 -07:00
Asier Isayas
16c7b2567b fix bug that blocked local mongo proxy and cassandra proxy development 2024-04-09 11:39:11 -04:00
Senthamil Sindhu
78d9a0cd8d Revert code 2024-04-08 16:20:40 -07:00
Senthamil Sindhu
c6ad538559 Run npm format and tests 2024-04-08 15:58:10 -07:00
Senthamil Sindhu
2bc09a6efe Add CP Prod endpoint 2024-04-08 15:37:19 -07:00
Senthamil Sindhu
d3a3033b25 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-04-08 15:32:50 -07:00
Asier Isayas
6bdc714e11 activate Mongo Proxy and Cassandra Proxy in Prod 2024-04-08 16:52:09 -04:00
Senthamil Sindhu
5042f28229 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-03-25 15:11:53 -07:00
Senthamil Sindhu
e1430fd06f Fix API endpoint for CassandraProxy query API 2024-03-18 10:25:17 -07:00
45 changed files with 695 additions and 726 deletions

View File

@@ -113,7 +113,7 @@ jobs:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps: steps:
- uses: nuget/setup-nuget@v2 - uses: nuget/setup-nuget@v1
with: with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }} nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder - name: Download Dist Folder
@@ -137,7 +137,7 @@ jobs:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps: steps:
- uses: nuget/setup-nuget@v2 - uses: nuget/setup-nuget@v1
with: with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }} nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder - name: Download Dist Folder

99
package-lock.json generated
View File

@@ -122,7 +122,7 @@
"@babel/preset-env": "7.24.7", "@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7", "@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7", "@babel/preset-typescript": "7.24.7",
"@playwright/test": "1.49.1", "@playwright/test": "1.44.0",
"@testing-library/react": "11.2.3", "@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7", "@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56", "@types/codemirror": "0.0.56",
@@ -10175,18 +10175,34 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.49.1", "version": "1.44.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.49.1" "playwright": "1.44.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=16"
}
},
"node_modules/@playwright/test/node_modules/playwright": {
"version": "1.44.0",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
} }
}, },
"node_modules/@polka/url": { "node_modules/@polka/url": {
@@ -14788,15 +14804,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"optional": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bintrees": { "node_modules/bintrees": {
"version": "1.0.2", "version": "1.0.2",
"license": "MIT" "license": "MIT"
@@ -19459,12 +19466,6 @@
"version": "2.0.5", "version": "2.0.5",
"license": "MIT" "license": "MIT"
}, },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"optional": true
},
"node_modules/filesize": { "node_modules/filesize": {
"version": "8.0.7", "version": "8.0.7",
"dev": true, "dev": true,
@@ -20053,19 +20054,6 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC" "license": "ISC"
}, },
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"license": "MIT", "license": "MIT",
@@ -24172,24 +24160,6 @@
"fsevents": "^1.2.7" "fsevents": "^1.2.7"
} }
}, },
"node_modules/jest-haste-map/node_modules/fsevents": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"deprecated": "Upgrade to fsevents v2 to mitigate potential security issues",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"dependencies": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
},
"engines": {
"node": ">= 4.0"
}
},
"node_modules/jest-haste-map/node_modules/jest-worker": { "node_modules/jest-haste-map/node_modules/jest-worker": {
"version": "24.9.0", "version": "24.9.0",
"license": "MIT", "license": "MIT",
@@ -30828,34 +30798,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/playwright": {
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
"dev": true,
"dependencies": {
"playwright-core": "1.49.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.49.1", "version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=16"
} }
}, },
"node_modules/plotly.js-cartesian-dist-min": { "node_modules/plotly.js-cartesian-dist-min": {

View File

@@ -117,7 +117,7 @@
"@babel/preset-env": "7.24.7", "@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7", "@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7", "@babel/preset-typescript": "7.24.7",
"@playwright/test": "1.49.1", "@playwright/test": "1.44.0",
"@testing-library/react": "11.2.3", "@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7", "@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56", "@types/codemirror": "0.0.56",
@@ -170,10 +170,10 @@
"jest": "29.7.0", "jest": "29.7.0",
"jest-canvas-mock": "2.5.2", "jest-canvas-mock": "2.5.2",
"jest-circus": "29.7.0", "jest-circus": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-html-loader": "1.0.0", "jest-html-loader": "1.0.0",
"jest-react-hooks-shallow": "1.5.1", "jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "3.0.2", "jest-trx-results-processor": "3.0.2",
"jest-environment-jsdom": "29.7.0",
"less": "3.8.1", "less": "3.8.1",
"less-loader": "11.1.3", "less-loader": "11.1.3",
"less-vars-loader": "1.1.0", "less-vars-loader": "1.1.0",

View File

@@ -149,7 +149,7 @@ export class PortalBackendEndpoints {
} }
export class MongoProxyEndpoints { export class MongoProxyEndpoints {
public static readonly Development: string = "https://localhost:7238"; public static readonly Local: string = "https://localhost:7238";
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";

View File

@@ -13,6 +13,7 @@ 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";
import { getErrorMessage } from "./ErrorHandlingUtils"; import { getErrorMessage } from "./ErrorHandlingUtils";
import { runCommand } from "hooks/useDatabaseAccounts";
const _global = typeof self === "undefined" ? window : self; const _global = typeof self === "undefined" ? window : self;
@@ -32,6 +33,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
return null; return null;
} }
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`; const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
console.log("AAD Token ", userContext.aadToken);
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`; const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
return authorizationToken; return authorizationToken;
} }
@@ -189,19 +191,10 @@ let _client: Cosmos.CosmosClient;
export function client(): Cosmos.CosmosClient { export function client(): Cosmos.CosmosClient {
if (_client) { if (_client) {
if (!userContext.refreshCosmosClient) { if (!userContext.hasDataPlaneRbacSettingChanged) {
return _client; return _client;
} }
_client.dispose();
_client = null;
} }
if (userContext.refreshCosmosClient) {
updateUserContext({
refreshCosmosClient: false,
});
}
let _defaultHeaders: Cosmos.CosmosHeaders = {}; let _defaultHeaders: Cosmos.CosmosHeaders = {};
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] = _defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge; SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
@@ -218,10 +211,14 @@ export function client(): Cosmos.CosmosClient {
_defaultHeaders["x-ms-cosmos-priority-level"] = PriorityLevel.Default; _defaultHeaders["x-ms-cosmos-priority-level"] = PriorityLevel.Default;
} }
const wrappedTokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
return await runCommand(tokenProvider, requestInfo);
};
const options: Cosmos.CosmosClientOptions = { const options: Cosmos.CosmosClientOptions = {
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
key: userContext.dataPlaneRbacEnabled ? "" : userContext.masterKey, key: userContext.dataPlaneRbacEnabled ? "" : userContext.masterKey,
tokenProvider, tokenProvider: wrappedTokenProvider,
userAgentSuffix: "Azure Portal", userAgentSuffix: "Azure Portal",
defaultHeaders: _defaultHeaders, defaultHeaders: _defaultHeaders,
connectionPolicy: { connectionPolicy: {

View File

@@ -1,4 +1,4 @@
// import { QueryOperationOptions } from "@azure/cosmos"; import { QueryOperationOptions } from "@azure/cosmos";
import { QueryResults } from "../Contracts/ViewModels"; import { QueryResults } from "../Contracts/ViewModels";
interface QueryResponse { interface QueryResponse {
@@ -11,13 +11,17 @@ interface QueryResponse {
} }
export interface MinimalQueryIterator { export interface MinimalQueryIterator {
fetchNext: () => Promise<QueryResponse>; fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
} }
// Pick<QueryIterator<any>, "fetchNext">; // Pick<QueryIterator<any>, "fetchNext">;
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> { export function nextPage(
return documentsIterator.fetchNext().then((response) => { documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> {
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
const documents = response.resources; const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any

View File

@@ -722,63 +722,63 @@ export function getEndpoint(endpoint: string): string {
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean { export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
const mongoProxyEnvironmentMap: { [key: string]: string[] } = { const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
[MongoProxyApi.ResourceList]: [ [MongoProxyApi.ResourceList]: [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake, MongoProxyEndpoints.Mooncake,
], ],
[MongoProxyApi.QueryDocuments]: [ [MongoProxyApi.QueryDocuments]: [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake, MongoProxyEndpoints.Mooncake,
], ],
[MongoProxyApi.CreateDocument]: [ [MongoProxyApi.CreateDocument]: [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake, MongoProxyEndpoints.Mooncake,
], ],
[MongoProxyApi.ReadDocument]: [ [MongoProxyApi.ReadDocument]: [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake, MongoProxyEndpoints.Mooncake,
], ],
[MongoProxyApi.UpdateDocument]: [ [MongoProxyApi.UpdateDocument]: [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake, MongoProxyEndpoints.Mooncake,
], ],
[MongoProxyApi.DeleteDocument]: [ [MongoProxyApi.DeleteDocument]: [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake, MongoProxyEndpoints.Mooncake,
], ],
[MongoProxyApi.CreateCollectionWithProxy]: [ [MongoProxyApi.CreateCollectionWithProxy]: [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake, MongoProxyEndpoints.Mooncake,
], ],
[MongoProxyApi.LegacyMongoShell]: [ [MongoProxyApi.LegacyMongoShell]: [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake, MongoProxyEndpoints.Mooncake,
], ],
[MongoProxyApi.BulkDelete]: [ [MongoProxyApi.BulkDelete]: [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,

View File

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

View File

@@ -9,7 +9,6 @@ export enum TabKind {
Graph, Graph,
SQLQuery, SQLQuery,
ScaleSettings, ScaleSettings,
MongoQuery,
} }
/** /**
@@ -52,8 +51,6 @@ export interface OpenCollectionTab extends OpenTab {
*/ */
export interface OpenQueryTab extends OpenCollectionTab { export interface OpenQueryTab extends OpenCollectionTab {
query: QueryInfo; query: QueryInfo;
splitterDirection?: "vertical" | "horizontal";
queryViewSizePercent?: number;
} }
/** /**

View File

@@ -115,13 +115,7 @@ export interface CollectionBase extends TreeNode {
isSampleCollection?: boolean; isSampleCollection?: boolean;
onDocumentDBDocumentsClick(): void; onDocumentDBDocumentsClick(): void;
onNewQueryClick( onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
source: any,
event?: MouseEvent,
queryText?: string,
splitterDirection?: "horizontal" | "vertical",
queryViewSizePercent?: number,
): void;
expandCollection(): void; expandCollection(): void;
collapseCollection(): void; collapseCollection(): void;
getDatabase(): Database; getDatabase(): Database;
@@ -157,13 +151,7 @@ export interface Collection extends CollectionBase {
onSettingsClick: () => Promise<void>; onSettingsClick: () => Promise<void>;
onNewGraphClick(): void; onNewGraphClick(): void;
onNewMongoQueryClick( onNewMongoQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
source: any,
event?: MouseEvent,
queryText?: string,
splitterDirection?: "horizontal" | "vertical",
queryViewSizePercent?: number,
): void;
onNewMongoShellClick(): void; onNewMongoShellClick(): void;
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void; onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void; onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
@@ -323,8 +311,6 @@ export interface QueryTabOptions extends TabOptions {
partitionKey?: DataModels.PartitionKey; partitionKey?: DataModels.PartitionKey;
queryText?: string; queryText?: string;
resourceTokenPartitionKey?: string; resourceTokenPartitionKey?: string;
splitterDirection?: "horizontal" | "vertical";
queryViewSizePercent?: number;
} }
export interface ScriptTabOption extends TabOptions { export interface ScriptTabOption extends TabOptions {

View File

@@ -1134,7 +1134,7 @@ export default class Explorer {
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
userContext.authType === AuthType.ResourceToken userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow : this.refreshAllDatabases();
} }
await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount();

View File

@@ -1,5 +1,4 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. // TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
import { configContext, Platform } from "ConfigContext";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import React from "react"; import React from "react";
import { ActionContracts } from "../../Contracts/ExplorerContracts"; import { ActionContracts } from "../../Contracts/ExplorerContracts";
@@ -57,19 +56,6 @@ function openCollectionTab(
continue; continue;
} }
if (
configContext.platform === Platform.Fabric &&
!(
// whitelist the tab kinds that are allowed to be opened in Fabric
(
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
action.tabKind === ActionContracts.TabKind.SQLQuery
)
)
) {
continue;
}
//expand database first if not expanded to load the collections //expand database first if not expanded to load the collections
if (!database.isDatabaseExpanded?.()) { if (!database.isDatabaseExpanded?.()) {
database.expandDatabase?.(); database.expandDatabase?.();
@@ -135,28 +121,10 @@ function openCollectionTab(
action.tabKind === ActionContracts.TabKind.SQLQuery || action.tabKind === ActionContracts.TabKind.SQLQuery ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery] action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
) { ) {
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
collection.onNewQueryClick( collection.onNewQueryClick(
collection, collection,
undefined, undefined,
generateQueryText(openQueryTabAction, collection.partitionKeyProperties), generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
openQueryTabAction.splitterDirection,
openQueryTabAction.queryViewSizePercent,
);
break;
}
if (
action.tabKind === ActionContracts.TabKind.MongoQuery ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoQuery]
) {
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
collection.onNewMongoQueryClick(
collection,
undefined,
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
openQueryTabAction.splitterDirection,
openQueryTabAction.queryViewSizePercent,
); );
break; break;
} }

View File

@@ -819,9 +819,22 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowAnalyticalStoreOptions() && ( {this.shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Stack horizontal>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
{this.getAnalyticalStorageContent()} Analytical store
</Text> </Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getAnalyticalStorageTooltipContent()}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
/>
</TooltipHost>
</Stack>
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<div role="radiogroup"> <div role="radiogroup">
@@ -1217,7 +1230,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return ""; return "";
} }
private getAnalyticalStorageContent(): JSX.Element { private getAnalyticalStorageTooltipContent(): JSX.Element {
return ( return (
<Text variant="small"> <Text variant="small">
Enable analytical store capability to perform near real-time analytics on your operational data, without Enable analytical store capability to perform near real-time analytics on your operational data, without

View File

@@ -193,17 +193,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
if (
enableDataPlaneRBACOption !== LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) ||
retryAttempts !== LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts) ||
retryInterval !== LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval) ||
MaxWaitTimeInSeconds !== LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds)
) {
updateUserContext({
refreshCosmosClient: true,
});
}
if (configContext.platform !== Platform.Fabric) { if (configContext.platform !== Platform.Fabric) {
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption); LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
if ( if (
@@ -213,6 +202,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
) { ) {
updateUserContext({ updateUserContext({
dataPlaneRbacEnabled: true, dataPlaneRbacEnabled: true,
hasDataPlaneRbacSettingChanged: true,
}); });
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true }); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
try { try {
@@ -236,6 +226,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
} else { } else {
updateUserContext({ updateUserContext({
dataPlaneRbacEnabled: false, dataPlaneRbacEnabled: false,
hasDataPlaneRbacSettingChanged: true,
}); });
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) { if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
@@ -573,6 +564,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
)} )}
{userContext.apiType === "SQL" && ( {userContext.apiType === "SQL" && (
<> <>
<AccordionItem value="3"> <AccordionItem value="3">
@@ -671,7 +663,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem> </AccordionItem>
</> </>
)} )}
{(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") && (
<AccordionItem value="6"> <AccordionItem value="6">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Retry Settings</div> <div className={styles.header}>Retry Settings</div>
@@ -703,8 +695,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<div> <div>
<span className={styles.subHeader}>Fixed retry interval (ms)</span> <span className={styles.subHeader}>Fixed retry interval (ms)</span>
<InfoTooltip className={styles.headerIcon}> <InfoTooltip className={styles.headerIcon}>
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as
as part of the response. Default value is 0 milliseconds. part of the response. Default value is 0 milliseconds.
</InfoTooltip> </InfoTooltip>
</div> </div>
<SpinButton <SpinButton
@@ -743,7 +735,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</div> </div>
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
)}
<AccordionItem value="7"> <AccordionItem value="7">
<AccordionHeader> <AccordionHeader>
@@ -767,6 +758,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</div> </div>
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
{shouldShowCrossPartitionOption && ( {shouldShowCrossPartitionOption && (
<AccordionItem value="8"> <AccordionItem value="8">
<AccordionHeader> <AccordionHeader>
@@ -792,6 +784,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
)} )}
{shouldShowParallelismOption && ( {shouldShowParallelismOption && (
<AccordionItem value="9"> <AccordionItem value="9">
<AccordionHeader> <AccordionHeader>
@@ -825,6 +818,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
)} )}
{shouldShowPriorityLevelOption && ( {shouldShowPriorityLevelOption && (
<AccordionItem value="10"> <AccordionItem value="10">
<AccordionHeader> <AccordionHeader>
@@ -848,6 +842,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
)} )}
{shouldShowGraphAutoVizOption && ( {shouldShowGraphAutoVizOption && (
<AccordionItem value="11"> <AccordionItem value="11">
<AccordionHeader> <AccordionHeader>
@@ -869,6 +864,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
)} )}
{shouldShowCopilotSampleDBOption && ( {shouldShowCopilotSampleDBOption && (
<AccordionItem value="12"> <AccordionItem value="12">
<AccordionHeader> <AccordionHeader>

View File

@@ -308,11 +308,18 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
</Stack> </Stack>
<Stack <Stack
className="panelGroupSpacing" className="panelGroupSpacing"
>
<Stack
horizontal={true}
> >
<Text <Text
className="panelTextBold" className="panelTextBold"
variant="small" variant="small"
> >
Analytical store
</Text>
<StyledTooltipHostBase
content={
<Text <Text
variant="small" variant="small"
> >
@@ -325,7 +332,17 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
Learn more Learn more
</StyledLinkBase> </StyledLinkBase>
</Text> </Text>
</Text> }
directionalHint={4}
>
<Icon
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
/>
</StyledTooltipHostBase>
</Stack>
<Stack <Stack
horizontal={true} horizontal={true}
verticalAlign="center" verticalAlign="center"

View File

@@ -3,11 +3,17 @@
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent"; import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
import { import {
AppStateComponentNames, AppStateComponentNames,
deleteSubComponentState, deleteState,
readSubComponentState, loadState,
saveSubComponentState, saveState,
saveStateDebounced,
} from "Shared/AppStatePersistenceUtility"; } from "Shared/AppStatePersistenceUtility";
import { userContext } from "UserContext";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
const componentName = AppStateComponentNames.DocumentsTab;
export enum SubComponentName { export enum SubComponentName {
ColumnSizes = "ColumnSizes", ColumnSizes = "ColumnSizes",
@@ -15,7 +21,6 @@ export enum SubComponentName {
MainTabDivider = "MainTabDivider", MainTabDivider = "MainTabDivider",
ColumnsSelection = "ColumnsSelection", ColumnsSelection = "ColumnsSelection",
ColumnSort = "ColumnSort", ColumnSort = "ColumnSort",
CurrentFilter = "CurrentFilter",
} }
export type ColumnSizesMap = { [columnId: string]: WidthDefinition }; export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
@@ -25,22 +30,84 @@ export type TabDivider = { leftPaneWidthPercent: number };
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] }; export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" }; export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
// Wrap the ...SubComponentState functions for type safety /**
*
export const readDocumentsTabSubComponentState = <T>( * @param subComponentName
* @param collection
* @param defaultValue Will be returned if persisted state is not found
* @returns
*/
export const readSubComponentState = <T>(
subComponentName: SubComponentName, subComponentName: SubComponentName,
collection: ViewModels.CollectionBase, collection: ViewModels.CollectionBase,
defaultValue: T, defaultValue: T,
): T => readSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, defaultValue); ): T => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
return defaultValue;
}
export const saveDocumentsTabSubComponentState = <T>( const state = loadState({
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection.databaseId,
containerName: collection.id(),
}) as T;
return state || defaultValue;
};
/**
*
* @param subComponentName
* @param collection
* @param state State to save
* @param debounce true for high-frequency calls (e.g mouse drag events)
*/
export const saveSubComponentState = <T>(
subComponentName: SubComponentName, subComponentName: SubComponentName,
collection: ViewModels.CollectionBase, collection: ViewModels.CollectionBase,
state: T, state: T,
debounce?: boolean, debounce?: boolean,
): void => saveSubComponentState<T>(AppStateComponentNames.DocumentsTab, subComponentName, collection, state, debounce); ): void => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
return;
}
export const deleteDocumentsTabSubComponentState = ( (debounce ? saveStateDebounced : saveState)(
subComponentName: SubComponentName, {
collection: ViewModels.CollectionBase, componentName: componentName,
) => deleteSubComponentState(AppStateComponentNames.DocumentsTab, subComponentName, collection); subComponentName,
globalAccountName,
databaseName: collection.databaseId,
containerName: collection.id(),
},
state,
);
};
export const deleteSubComponentState = (subComponentName: SubComponentName, collection: ViewModels.CollectionBase) => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
return;
}
deleteState({
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection.databaseId,
containerName: collection.id(),
});
};

View File

@@ -21,7 +21,6 @@ import { queryDocuments } from "Common/dataAccess/queryDocuments";
import { readDocument } from "Common/dataAccess/readDocument"; import { readDocument } from "Common/dataAccess/readDocument";
import { updateDocument } from "Common/dataAccess/updateDocument"; import { updateDocument } from "Common/dataAccess/updateDocument";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
@@ -35,9 +34,8 @@ import {
FilterHistory, FilterHistory,
SubComponentName, SubComponentName,
TabDivider, TabDivider,
deleteDocumentsTabSubComponentState, readSubComponentState,
readDocumentsTabSubComponentState, saveSubComponentState,
saveDocumentsTabSubComponentState,
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil"; } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper"; import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil"; import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
@@ -142,8 +140,6 @@ export class DocumentsTabV2 extends TabsBase {
private title: string; private title: string;
private resourceTokenPartitionKey: string; private resourceTokenPartitionKey: string;
protected persistedState: OpenCollectionTab;
constructor(options: ViewModels.DocumentsTabOptions) { constructor(options: ViewModels.DocumentsTabOptions) {
super(options); super(options);
@@ -151,13 +147,6 @@ export class DocumentsTabV2 extends TabsBase {
this.title = options.title; this.title = options.title;
this.partitionKey = options.partitionKey; this.partitionKey = options.partitionKey;
this.resourceTokenPartitionKey = options.resourceTokenPartitionKey; this.resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.persistedState = {
actionType: ActionType.OpenCollectionTab,
tabKind: options.isPreferredApiMongoDB ? TabKind.MongoDocuments : TabKind.SQLDocuments,
databaseResourceId: options.collection.databaseId,
collectionResourceId: options.collection.id(),
};
} }
public render(): JSX.Element { public render(): JSX.Element {
@@ -586,10 +575,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
onIsExecutingChange, onIsExecutingChange,
isTabActive, isTabActive,
}): JSX.Element => { }): JSX.Element => {
const [filterContent, setFilterContent] = useState<string>(() => const [filterContent, setFilterContent] = useState<string>("");
readDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, ""),
);
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]); const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
const [isExecuting, setIsExecuting] = useState<boolean>(false); const [isExecuting, setIsExecuting] = useState<boolean>(false);
const styles = useDocumentsTabStyles(); const styles = useDocumentsTabStyles();
@@ -620,7 +606,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
// State // State
const [tabStateData, setTabStateData] = useState<TabDivider>(() => const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
readDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, { readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
leftPaneWidthPercent: 35, leftPaneWidthPercent: 35,
}), }),
); );
@@ -635,7 +621,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
// User's filter history // User's filter history
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() => const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
readDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory), readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
); );
// For progress bar for bulk delete (noSql) // For progress bar for bulk delete (noSql)
@@ -777,7 +763,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}; };
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => { const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>( const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
SubComponentName.ColumnsSelection, SubComponentName.ColumnsSelection,
_collection, _collection,
undefined, undefined,
@@ -822,7 +808,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
useEffect(() => { useEffect(() => {
setKeyboardActions({ setKeyboardActions({
[KeyboardAction.CLEAR_SEARCH]: () => { [KeyboardAction.CLEAR_SEARCH]: () => {
updateFilterContent(""); setFilterContent("");
refreshDocumentsGrid(true); refreshDocumentsGrid(true);
return true; return true;
}, },
@@ -1659,7 +1645,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness // Column definition is a map<id, ColumnDefinition> to garantee uniqueness
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => { const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
const persistedColumnsSelection = readDocumentsTabSubComponentState<ColumnsSelection>( const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
SubComponentName.ColumnsSelection, SubComponentName.ColumnsSelection,
_collection, _collection,
undefined, undefined,
@@ -1970,7 +1956,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT); const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
setLastFilterContents(limitedLastFilterContents); setLastFilterContents(limitedLastFilterContents);
saveDocumentsTabSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents); saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
}; };
const refreshDocumentsGrid = useCallback( const refreshDocumentsGrid = useCallback(
@@ -2027,7 +2013,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
setSelectedColumnIds(newSelectedColumnIds); setSelectedColumnIds(newSelectedColumnIds);
saveDocumentsTabSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, { saveSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
selectedColumnIds: newSelectedColumnIds, selectedColumnIds: newSelectedColumnIds,
columnDefinitions, columnDefinitions,
}); });
@@ -2077,15 +2063,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
return options; return options;
}; };
const updateFilterContent = (filter: string): void => {
if (filter === "" || filter === undefined) {
deleteDocumentsTabSubComponentState(SubComponentName.CurrentFilter, _collection);
} else {
saveDocumentsTabSubComponentState<string>(SubComponentName.CurrentFilter, _collection, filter, true);
}
setFilterContent(filter);
};
return ( return (
<CosmosFluentProvider className={styles.container}> <CosmosFluentProvider className={styles.container}>
<div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}> <div className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
@@ -2100,7 +2077,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
} }
title="Type a query predicate or choose one from the list." title="Type a query predicate or choose one from the list."
value={filterContent} value={filterContent}
onChange={updateFilterContent} onChange={(value) => setFilterContent(value)}
onKeyDown={onFilterKeyDown} onKeyDown={onFilterKeyDown}
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }} bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
/> />
@@ -2126,7 +2103,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
<Allotment <Allotment
onDragEnd={(sizes: number[]) => { onDragEnd={(sizes: number[]) => {
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]); tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
saveDocumentsTabSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData); saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
setTabStateData(tabStateData); setTabStateData(tabStateData);
}} }}
> >

View File

@@ -42,9 +42,9 @@ import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPan
import { import {
ColumnSizesMap, ColumnSizesMap,
ColumnSort, ColumnSort,
deleteDocumentsTabSubComponentState, deleteSubComponentState,
readDocumentsTabSubComponentState, readSubComponentState,
saveDocumentsTabSubComponentState, saveSubComponentState,
SubComponentName, SubComponentName,
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil"; } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
@@ -118,11 +118,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
const sortedRowsRef = React.useRef(null); const sortedRowsRef = React.useRef(null);
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => { const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
const columnSizesMap: ColumnSizesMap = readDocumentsTabSubComponentState( const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
SubComponentName.ColumnSizes,
collection,
{},
);
const columnSizesPx: TableColumnSizingOptions = {}; const columnSizesPx: TableColumnSizingOptions = {};
selectedColumnIds.forEach((columnId) => { selectedColumnIds.forEach((columnId) => {
if ( if (
@@ -146,7 +142,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
sortDirection: "ascending" | "descending"; sortDirection: "ascending" | "descending";
sortColumn: TableColumnId | undefined; sortColumn: TableColumnId | undefined;
}>(() => { }>(() => {
const sort = readDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined); const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
if (!sort) { if (!sort) {
return { return {
@@ -178,12 +174,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
return acc; return acc;
}, {} as ColumnSizesMap); }, {} as ColumnSizesMap);
saveDocumentsTabSubComponentState<ColumnSizesMap>( saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
SubComponentName.ColumnSizes,
collection,
persistentSizes,
true,
);
return newSizingOptions; return newSizingOptions;
}); });
@@ -195,14 +186,11 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
setColumnSort(event, columnId, direction); setColumnSort(event, columnId, direction);
if (columnId === undefined || direction === undefined) { if (columnId === undefined || direction === undefined) {
deleteDocumentsTabSubComponentState(SubComponentName.ColumnSort, collection); deleteSubComponentState(SubComponentName.ColumnSort, collection);
return; return;
} }
saveDocumentsTabSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { saveSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { columnId, direction });
columnId,
direction,
});
}; };
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes // Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes

View File

@@ -1,4 +1,3 @@
import { ActionType, TabKind } from "Contracts/ActionContracts";
import React from "react"; import React from "react";
import MongoUtility from "../../../Common/MongoUtility"; import MongoUtility from "../../../Common/MongoUtility";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
@@ -21,7 +20,7 @@ export class NewMongoQueryTab extends NewQueryTab {
private mongoQueryTabProps: IMongoQueryTabProps, private mongoQueryTabProps: IMongoQueryTabProps,
) { ) {
super(options, mongoQueryTabProps); super(options, mongoQueryTabProps);
this.queryText = options.queryText ?? ""; this.queryText = "";
this.iMongoQueryTabComponentProps = { this.iMongoQueryTabComponentProps = {
collection: options.collection, collection: options.collection,
isExecutionError: this.isExecutionError(), isExecutionError: this.isExecutionError(),
@@ -29,8 +28,6 @@ export class NewMongoQueryTab extends NewQueryTab {
tabsBaseInstance: this, tabsBaseInstance: this,
queryText: this.queryText, queryText: this.queryText,
partitionKey: this.partitionKey, partitionKey: this.partitionKey,
splitterDirection: options.splitterDirection,
queryViewSizePercent: options.queryViewSizePercent,
container: this.mongoQueryTabProps.container, container: this.mongoQueryTabProps.container,
onTabAccessor: (instance: ITabAccessor): void => { onTabAccessor: (instance: ITabAccessor): void => {
this.iTabAccessor = instance; this.iTabAccessor = instance;
@@ -38,26 +35,6 @@ export class NewMongoQueryTab extends NewQueryTab {
isPreferredApiMongoDB: true, isPreferredApiMongoDB: true,
monacoEditorSetting: "plaintext", monacoEditorSetting: "plaintext",
viewModelcollection: this.mongoQueryTabProps.viewModelcollection, viewModelcollection: this.mongoQueryTabProps.viewModelcollection,
onUpdatePersistedState: (state: {
queryText: string;
splitterDirection: string;
queryViewSizePercent: number;
}): void => {
this.persistedState = {
actionType: ActionType.OpenCollectionTab,
tabKind: TabKind.SQLQuery,
databaseResourceId: options.collection.databaseId,
collectionResourceId: options.collection.id(),
query: {
text: state.queryText,
},
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
queryViewSizePercent: state.queryViewSizePercent,
};
if (this.triggerPersistState) {
this.triggerPersistState();
}
},
}; };
} }

View File

@@ -1,5 +1,4 @@
import { sendMessage } from "Common/MessageHandler"; import { sendMessage } from "Common/MessageHandler";
import { ActionType, OpenQueryTab, TabKind } from "Contracts/ActionContracts";
import { MessageTypes } from "Contracts/MessageTypes"; import { MessageTypes } from "Contracts/MessageTypes";
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext"; import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
@@ -27,8 +26,6 @@ export class NewQueryTab extends TabsBase {
public iQueryTabComponentProps: IQueryTabComponentProps; public iQueryTabComponentProps: IQueryTabComponentProps;
public iTabAccessor: ITabAccessor; public iTabAccessor: ITabAccessor;
protected persistedState: OpenQueryTab;
constructor( constructor(
options: QueryTabOptions, options: QueryTabOptions,
private props: IQueryTabProps, private props: IQueryTabProps,
@@ -42,41 +39,12 @@ export class NewQueryTab extends TabsBase {
tabsBaseInstance: this, tabsBaseInstance: this,
queryText: options.queryText, queryText: options.queryText,
partitionKey: this.partitionKey, partitionKey: this.partitionKey,
splitterDirection: options.splitterDirection,
queryViewSizePercent: options.queryViewSizePercent,
container: this.props.container, container: this.props.container,
onTabAccessor: (instance: ITabAccessor): void => { onTabAccessor: (instance: ITabAccessor): void => {
this.iTabAccessor = instance; this.iTabAccessor = instance;
}, },
isPreferredApiMongoDB: false, isPreferredApiMongoDB: false,
onUpdatePersistedState: (state: {
queryText: string;
splitterDirection: string;
queryViewSizePercent: number;
}): void => {
this.persistedState = {
actionType: ActionType.OpenCollectionTab,
tabKind: TabKind.SQLQuery,
databaseResourceId: options.collection.databaseId,
collectionResourceId: options.collection.id(),
query: {
text: state.queryText,
},
splitterDirection: state.splitterDirection as "vertical" | "horizontal",
queryViewSizePercent: state.queryViewSizePercent,
}; };
if (this.triggerPersistState) {
this.triggerPersistState();
}
},
};
// set initial state
this.iQueryTabComponentProps.onUpdatePersistedState({
queryText: options.queryText,
splitterDirection: options.splitterDirection,
queryViewSizePercent: options.queryViewSizePercent,
});
} }
public render(): JSX.Element { public render(): JSX.Element {

View File

@@ -34,7 +34,6 @@ jest.mock("Shared/AppStatePersistenceUtility", () => ({
AppStateComponentNames: { AppStateComponentNames: {
QueryCopilot: "QueryCopilot", QueryCopilot: "QueryCopilot",
}, },
readSubComponentState: jest.fn(),
})); }));
describe("QueryTabComponent", () => { describe("QueryTabComponent", () => {

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */ /* eslint-disable no-console */
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
import QueryError, { createMonacoErrorLocationResolver, createMonacoMarkersForQueryErrors } from "Common/QueryError"; import QueryError, { createMonacoErrorLocationResolver, createMonacoMarkersForQueryErrors } from "Common/QueryError";
import { SplitterDirection } from "Common/Splitter"; import { SplitterDirection } from "Common/Splitter";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
@@ -18,7 +18,13 @@ import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction } from "KeyboardShortcuts"; import { KeyboardAction } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants"; import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import {
LocalStorageUtility,
StorageKey,
getDefaultQueryResultsView,
getRUThreshold,
ruThresholdEnabled,
} from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { Allotment } from "allotment"; import { Allotment } from "allotment";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
@@ -93,13 +99,6 @@ export interface IQueryTabComponentProps {
copilotEnabled?: boolean; copilotEnabled?: boolean;
isSampleCopilotActive?: boolean; isSampleCopilotActive?: boolean;
copilotStore?: Partial<QueryCopilotState>; copilotStore?: Partial<QueryCopilotState>;
splitterDirection?: "horizontal" | "vertical";
queryViewSizePercent?: number;
onUpdatePersistedState: (state: {
queryText: string;
splitterDirection: "vertical" | "horizontal";
queryViewSizePercent: number;
}) => void;
} }
interface IQueryTabStates { interface IQueryTabStates {
@@ -119,13 +118,11 @@ interface IQueryTabStates {
queryResultsView: SplitterDirection; queryResultsView: SplitterDirection;
errors?: QueryError[]; errors?: QueryError[];
modelMarkers?: monaco.editor.IMarkerData[]; modelMarkers?: monaco.editor.IMarkerData[];
queryViewSizePercent: number;
} }
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => { export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
const styles = useQueryTabStyles(); const styles = useQueryTabStyles();
const copilotStore = useCopilotStore(); const copilotStore = useCopilotStore();
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected(); const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
const queryTabProps = { const queryTabProps = {
...props, ...props,
@@ -135,12 +132,12 @@ export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any =>
isSampleCopilotActive: isSampleCopilotActive, isSampleCopilotActive: isSampleCopilotActive,
copilotStore: copilotStore, copilotStore: copilotStore,
}; };
return <QueryTabComponentImpl styles={styles} {...queryTabProps} />; return <QueryTabComponentImpl styles={styles} {...queryTabProps}></QueryTabComponentImpl>;
}; };
export const QueryTabComponent = (props: IQueryTabComponentProps): any => { export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
const styles = useQueryTabStyles(); const styles = useQueryTabStyles();
return <QueryTabComponentImpl styles={styles} {...{ ...props }} />; return <QueryTabComponentImpl styles={styles} {...props}></QueryTabComponentImpl>;
}; };
type QueryTabComponentImplProps = IQueryTabComponentProps & { type QueryTabComponentImplProps = IQueryTabComponentProps & {
@@ -149,8 +146,6 @@ type QueryTabComponentImplProps = IQueryTabComponentProps & {
// Inner (legacy) class component. We only use this component via one of the two functional components above (since we need to use the `useQueryTabStyles` hook). // Inner (legacy) class component. We only use this component via one of the two functional components above (since we need to use the `useQueryTabStyles` hook).
class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps, IQueryTabStates> { class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps, IQueryTabStates> {
private static readonly DEBOUNCE_DELAY_MS = 1000;
public queryEditorId: string; public queryEditorId: string;
public executeQueryButton: Button; public executeQueryButton: Button;
public saveQueryButton: Button; public saveQueryButton: Button;
@@ -162,10 +157,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
private _iterator: MinimalQueryIterator; private _iterator: MinimalQueryIterator;
private queryAbortController: AbortController; private queryAbortController: AbortController;
queryEditor: React.RefObject<EditorReact>; queryEditor: React.RefObject<EditorReact>;
private timeoutId: NodeJS.Timeout | undefined;
constructor(props: QueryTabComponentImplProps) { constructor(props: QueryTabComponentImplProps) {
super(props); super(props);
this.queryEditor = createRef<EditorReact>(); this.queryEditor = createRef<EditorReact>();
this.state = { this.state = {
@@ -181,9 +176,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
cancelQueryTimeoutID: undefined, cancelQueryTimeoutID: undefined,
copilotActive: this._queryCopilotActive(), copilotActive: this._queryCopilotActive(),
currentTabActive: true, currentTabActive: true,
queryResultsView: queryResultsView: getDefaultQueryResultsView(),
props.splitterDirection === "vertical" ? SplitterDirection.Vertical : SplitterDirection.Horizontal,
queryViewSizePercent: props.queryViewSizePercent,
}; };
this.isCloseClicked = false; this.isCloseClicked = false;
this.splitterId = this.props.tabId + "_splitter"; this.splitterId = this.props.tabId + "_splitter";
@@ -214,23 +207,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}); });
} }
/**
* Helper function to save the query text in the query tab state
* Since it reads and writes to the same state, it is debounced
*/
private saveQueryTabStateDebounced = () => {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.timeoutId = setTimeout(async () => {
this.props.onUpdatePersistedState({
queryText: this.state.sqlQueryEditorContent,
splitterDirection: this.state.queryResultsView,
queryViewSizePercent: this.state.queryViewSizePercent,
});
}, QueryTabComponentImpl.DEBOUNCE_DELAY_MS);
};
private _queryCopilotActive(): boolean { private _queryCopilotActive(): boolean {
if (this.props.copilotEnabled) { if (this.props.copilotEnabled) {
return readCopilotToggleStatus(userContext.databaseAccount); return readCopilotToggleStatus(userContext.databaseAccount);
@@ -368,19 +344,19 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
isExecutionError: false, isExecutionError: false,
}); });
// let queryOperationOptions: QueryOperationOptions; let queryOperationOptions: QueryOperationOptions;
// if (userContext.apiType === "SQL" && ruThresholdEnabled()) { if (userContext.apiType === "SQL" && ruThresholdEnabled()) {
// const ruThreshold: number = getRUThreshold(); const ruThreshold: number = getRUThreshold();
// queryOperationOptions = { queryOperationOptions = {
// ruCapPerOperation: ruThreshold, ruCapPerOperation: ruThreshold,
// } as QueryOperationOptions; } as QueryOperationOptions;
// } }
const queryDocuments = async (firstItemIndex: number) => const queryDocuments = async (firstItemIndex: number) =>
await queryDocumentsPage( await queryDocumentsPage(
this.props.collection && this.props.collection.id(), this.props.collection && this.props.collection.id(),
this._iterator, this._iterator,
firstItemIndex, firstItemIndex,
// queryOperationOptions, queryOperationOptions,
); );
this.props.tabsBaseInstance.isExecuting(true); this.props.tabsBaseInstance.isExecuting(true);
this.setState({ this.setState({
@@ -591,7 +567,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}; };
} }
private _setViewLayout(direction: SplitterDirection): void { private _setViewLayout(direction: SplitterDirection): void {
this.setState({ queryResultsView: direction }, () => this.saveQueryTabStateDebounced()); this.setState({ queryResultsView: direction });
// We'll need to refresh the context buttons to update the selected state of the view buttons // We'll need to refresh the context buttons to update the selected state of the view buttons
setTimeout(() => { setTimeout(() => {
@@ -623,16 +599,13 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
if (this.state.copilotActive) { if (this.state.copilotActive) {
this.props.copilotStore?.setQuery(newContent); this.props.copilotStore?.setQuery(newContent);
} }
this.setState( this.setState({
{
sqlQueryEditorContent: newContent, sqlQueryEditorContent: newContent,
queryCopilotGeneratedQuery: "", queryCopilotGeneratedQuery: "",
// Clear the markers when the user edits the document. // Clear the markers when the user edits the document.
modelMarkers: [], modelMarkers: [],
}, });
() => this.saveQueryTabStateDebounced(),
);
if (this.isPreferredApiMongoDB) { if (this.isPreferredApiMongoDB) {
if (newContent.length > 0) { if (newContent.length > 0) {
this.executeQueryButton = { this.executeQueryButton = {
@@ -731,20 +704,8 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
></QueryCopilotPromptbar> ></QueryCopilotPromptbar>
)} )}
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */} {/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
<Allotment <Allotment key={vertical.toString()} vertical={vertical}>
key={vertical.toString()} <Allotment.Pane data-test="QueryTab/EditorPane">
vertical={vertical}
onDragEnd={(sizes: number[]) => {
const queryViewSizePercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
this.setState({ queryViewSizePercent }, () => this.saveQueryTabStateDebounced());
}}
>
<Allotment.Pane
data-test="QueryTab/EditorPane"
preferredSize={
this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined
}
>
<EditorReact <EditorReact
ref={this.queryEditor} ref={this.queryEditor}
className={this.props.styles.queryEditor} className={this.props.styles.queryEditor}

View File

@@ -1,4 +1,3 @@
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
import React from "react"; import React from "react";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { SettingsComponent } from "../Controls/Settings/SettingsComponent"; import { SettingsComponent } from "../Controls/Settings/SettingsComponent";
@@ -11,18 +10,6 @@ export class SettingsTabV2 extends TabsBase {
} }
export class CollectionSettingsTabV2 extends SettingsTabV2 { export class CollectionSettingsTabV2 extends SettingsTabV2 {
protected persistedState: OpenCollectionTab;
constructor(options: ViewModels.TabOptions) {
super(options);
this.persistedState = {
actionType: ActionType.OpenCollectionTab,
tabKind: TabKind.ScaleSettings,
databaseResourceId: options.collection.databaseId,
collectionResourceId: options.collection.id(),
};
}
public onActivate(): void { public onActivate(): void {
super.onActivate(); super.onActivate();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);

View File

@@ -1,7 +1,9 @@
import { IMessageBarStyles, MessageBar, MessageBarType } from "@fluentui/react"; import { IMessageBarStyles, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants"; import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
import { sendMessage } from "Common/MessageHandler";
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
import { IpRule } from "Contracts/DataModels"; import { IpRule } from "Contracts/DataModels";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { CollectionTabKind } from "Contracts/ViewModels"; import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@@ -33,7 +35,7 @@ interface TabsProps {
} }
export const Tabs = ({ explorer }: TabsProps): JSX.Element => { export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs(); const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
const [ const [
showMongoAndCassandraProxiesNetworkSettingsWarningState, showMongoAndCassandraProxiesNetworkSettingsWarningState,
setShowMongoAndCassandraProxiesNetworkSettingsWarningState, setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
@@ -58,6 +60,29 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
return ( return (
<div className="tabsManagerContainer"> <div className="tabsManagerContainer">
{networkSettingsWarning && (
<MessageBar
messageBarType={MessageBarType.warning}
styles={defaultMessageBarStyles}
actions={
<MessageBarButton
onClick={() =>
sendMessage({
type:
userContext.apiType === "VCoreMongo"
? MessageTypes.OpenVCoreMongoNetworkingBlade
: MessageTypes.OpenPostgresNetworkingBlade,
})
}
>
Change network settings
</MessageBarButton>
}
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
>
{networkSettingsWarning}
</MessageBar>
)}
{showMongoAndCassandraProxiesNetworkSettingsWarningState && ( {showMongoAndCassandraProxiesNetworkSettingsWarningState && (
<MessageBar <MessageBar
messageBarType={MessageBarType.warning} messageBarType={MessageBarType.warning}
@@ -318,7 +343,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => { const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules; const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
if ( if (
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) || ((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local) ||
(userContext.apiType === "Cassandra" && (userContext.apiType === "Cassandra" &&
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) && configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
ipRules?.length ipRules?.length

View File

@@ -1,4 +1,3 @@
import { OpenTab } from "Contracts/ActionContracts";
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
@@ -31,8 +30,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
protected _theme: string; protected _theme: string;
public onLoadStartKey: number; public onLoadStartKey: number;
protected persistedState: OpenTab | undefined = undefined; // Used to store state of tab for persistence
constructor(options: ViewModels.TabOptions) { constructor(options: ViewModels.TabOptions) {
super(); super();
this.index = options.index; this.index = options.index;
@@ -58,10 +55,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}; };
} }
// Called by useTabs to persist
public getPersistedState = (): OpenTab | null => this.persistedState;
public triggerPersistState: () => void = undefined;
public onCloseTabButtonClick(): void { public onCloseTabButtonClick(): void {
useTabs.getState().closeTab(this); useTabs.getState().closeTab(this);
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, { TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {

View File

@@ -630,13 +630,7 @@ export default class Collection implements ViewModels.Collection {
} }
}; };
public onNewQueryClick( public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
source: any,
event: MouseEvent,
queryText?: string,
splitterDirection?: "horizontal" | "vertical",
queryViewSizePercent?: number,
) {
const collection: ViewModels.Collection = source.collection || source; const collection: ViewModels.Collection = source.collection || source;
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1; const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
const title = "Query " + id; const title = "Query " + id;
@@ -659,21 +653,13 @@ export default class Collection implements ViewModels.Collection {
queryText: queryText, queryText: queryText,
partitionKey: collection.partitionKey, partitionKey: collection.partitionKey,
onLoadStartKey: startKey, onLoadStartKey: startKey,
splitterDirection,
queryViewSizePercent,
}, },
{ container: this.container }, { container: this.container },
), ),
); );
} }
public onNewMongoQueryClick( public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
source: any,
event: MouseEvent,
queryText?: string,
splitterDirection?: "horizontal" | "vertical",
queryViewSizePercent?: number,
) {
const collection: ViewModels.Collection = source.collection || source; const collection: ViewModels.Collection = source.collection || source;
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1; const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
@@ -695,9 +681,6 @@ export default class Collection implements ViewModels.Collection {
node: this, node: this,
partitionKey: collection.partitionKey, partitionKey: collection.partitionKey,
onLoadStartKey: startKey, onLoadStartKey: startKey,
queryText,
splitterDirection,
queryViewSizePercent,
}, },
{ {
container: this.container, container: this.container,

View File

@@ -1,4 +1,4 @@
import { Link, Text } from "@fluentui/react"; import { initializeIcons, Link, Text } from "@fluentui/react";
import "bootstrap/dist/css/bootstrap.css"; import "bootstrap/dist/css/bootstrap.css";
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
@@ -20,7 +20,7 @@ const createAccountUrl = "https://aka.ms/cosmos-create-account-portal";
const onInit = async () => { const onInit = async () => {
const dataExplorerUrl = new URL("./", window.location.href).href; const dataExplorerUrl = new URL("./", window.location.href).href;
// initializeIcons(); initializeIcons();
await initializeConfiguration(); await initializeConfiguration();
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search); const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);

View File

@@ -1,4 +1,4 @@
// import { initializeIcons } from "@fluentui/react"; import { initializeIcons } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure"; import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure";
import * as React from "react"; import * as React from "react";
@@ -22,7 +22,7 @@ import { useAADAuth } from "./hooks/useAADAuth";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
import { useTokenMetadata } from "./hooks/usePortalAccessToken"; import { useTokenMetadata } from "./hooks/usePortalAccessToken";
// initializeIcons(); initializeIcons();
const App: React.FunctionComponent = () => { const App: React.FunctionComponent = () => {
// For handling encrypted portal tokens sent via query paramter // For handling encrypted portal tokens sent via query paramter

View File

@@ -2,7 +2,7 @@
import "./ReactDevTools"; import "./ReactDevTools";
// CSS Dependencies // CSS Dependencies
import { loadTheme } from "@fluentui/react"; import { initializeIcons, loadTheme } from "@fluentui/react";
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel"; import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial"; import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial"; import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
@@ -62,7 +62,7 @@ import "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
// initializeIcons(); initializeIcons();
const App: React.FunctionComponent = () => { const App: React.FunctionComponent = () => {
const isCarouselOpen = useCarousel((state) => state.shouldOpen); const isCarouselOpen = useCarousel((state) => state.shouldOpen);

View File

@@ -1,4 +1,4 @@
// import { initializeIcons } from "@fluentui/react"; import { initializeIcons } from "@fluentui/react";
import "bootstrap/dist/css/bootstrap.css"; import "bootstrap/dist/css/bootstrap.css";
import React from "react"; import React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
@@ -14,7 +14,7 @@ import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import * as GalleryUtils from "../Utils/GalleryUtils"; import * as GalleryUtils from "../Utils/GalleryUtils";
const onInit = async () => { const onInit = async () => {
// initializeIcons(); initializeIcons();
await initializeConfiguration(); await initializeConfiguration();
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search); const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search); const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);

View File

@@ -38,7 +38,6 @@ export type Features = {
readonly copilotChatFixedMonacoEditorHeight: boolean; readonly copilotChatFixedMonacoEditorHeight: boolean;
readonly enablePriorityBasedExecution: boolean; readonly enablePriorityBasedExecution: boolean;
readonly disableConnectionStringLogin: boolean; readonly disableConnectionStringLogin: boolean;
readonly restoreTabs: boolean;
// can be set via both flight and feature flag // can be set via both flight and feature flag
autoscaleDefault: boolean; autoscaleDefault: boolean;
@@ -109,7 +108,6 @@ 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"),
restoreTabs: "true" === get("restoretabs"),
}; };
} }

View File

@@ -1,4 +1,4 @@
import { Spinner, SpinnerSize } from "@fluentui/react"; import { initializeIcons, Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { withTranslation } from "react-i18next"; import { withTranslation } from "react-i18next";
@@ -13,7 +13,7 @@ import "./SelfServe.less";
import { SelfServeComponent } from "./SelfServeComponent"; import { SelfServeComponent } from "./SelfServeComponent";
import { SelfServeDescriptor } from "./SelfServeTypes"; import { SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeType } from "./SelfServeUtils"; import { SelfServeType } from "./SelfServeUtils";
// initializeIcons(); initializeIcons();
const loadTranslationFile = async (className: string): Promise<void> => { const loadTranslationFile = async (className: string): Promise<void> => {
const language = i18n.languages[0]; const language = i18n.languages[0];

View File

@@ -266,10 +266,7 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
method: "GET", method: "GET",
apiVersion: "2023-05-01-preview", apiVersion: "2023-05-01-preview",
queryParams: { queryParams: {
filter: filter: "armRegionNameeq '" + regionShortName + "'",
"armRegionNameeq '" +
regionShortName +
"' and productDisplayName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
}, },
}); });

View File

@@ -1,20 +1,12 @@
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { userContext } from "UserContext";
import * as ViewModels from "../Contracts/ViewModels";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
// The component name whose state is being saved. Component name must not include special characters. // The component name whose state is being saved. Component name must not include special characters.
export enum AppStateComponentNames { export enum AppStateComponentNames {
DocumentsTab = "DocumentsTab", DocumentsTab = "DocumentsTab",
MostRecentActivity = "MostRecentActivity", MostRecentActivity = "MostRecentActivity",
QueryCopilot = "QueryCopilot", QueryCopilot = "QueryCopilot",
DataExplorerAction = "DataExplorerAction",
} }
// Subcomponent for DataExplorerAction
export const OPEN_TABS_SUBCOMPONENT_NAME = "OpenTabs";
export const PATH_SEPARATOR = "/"; // export for testing purposes export const PATH_SEPARATOR = "/"; // export for testing purposes
const SCHEMA_VERSION = 1; const SCHEMA_VERSION = 1;
@@ -80,18 +72,12 @@ export const hasState = (path: StorePath): boolean => {
}; };
// This is for high-frequency state changes // This is for high-frequency state changes
// Keep track of timeouts per path let timeoutId: NodeJS.Timeout | undefined;
const pathToTimeoutIdMap = new Map<string, NodeJS.Timeout>();
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => { export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
const key = createKeyFromPath(path);
const timeoutId = pathToTimeoutIdMap.get(key);
if (timeoutId) { if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
pathToTimeoutIdMap.set( timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs);
key,
setTimeout(() => saveState(path, state), debounceDelayMs),
);
}; };
interface ApplicationState { interface ApplicationState {
@@ -126,93 +112,3 @@ export const createKeyFromPath = (path: StorePath): string => {
export const deleteAllStates = (): void => { export const deleteAllStates = (): void => {
LocalStorageUtility.removeEntry(StorageKey.AppState); LocalStorageUtility.removeEntry(StorageKey.AppState);
}; };
// Convenience functions
/**
*
* @param subComponentName
* @param collection
* @param defaultValue Will be returned if persisted state is not found
* @returns
*/
export const readSubComponentState = <T>(
componentName: AppStateComponentNames,
subComponentName: string,
collection: ViewModels.CollectionBase | undefined,
defaultValue: T,
): T => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.ReadPersistedTabState, { message, componentName });
return defaultValue;
}
const state = loadState({
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection ? collection.databaseId : "",
containerName: collection ? collection.id() : "",
}) as T;
return state || defaultValue;
};
/**
*
* @param subComponentName
* @param collection
* @param state State to save
* @param debounce true for high-frequency calls (e.g mouse drag events)
*/
export const saveSubComponentState = <T>(
componentName: AppStateComponentNames,
subComponentName: string,
collection: ViewModels.CollectionBase | undefined,
state: T,
debounce?: boolean,
): void => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.SavePersistedTabState, { message, componentName });
return;
}
(debounce ? saveStateDebounced : saveState)(
{
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection ? collection.databaseId : "",
containerName: collection ? collection.id() : "",
},
state,
);
};
export const deleteSubComponentState = (
componentName: AppStateComponentNames,
subComponentName: string,
collection: ViewModels.CollectionBase,
) => {
const globalAccountName = userContext.databaseAccount?.name;
if (!globalAccountName) {
const message = "Database account name not found in userContext";
console.error(message);
TelemetryProcessor.traceFailure(Action.DeletePersistedTabState, { message, componentName });
return;
}
deleteState({
componentName: componentName,
subComponentName,
globalAccountName,
databaseName: collection.databaseId,
containerName: collection.id(),
});
};

View File

@@ -81,6 +81,7 @@ export interface UserContext {
readonly endpoint?: string; readonly endpoint?: string;
readonly aadToken?: string; readonly aadToken?: string;
readonly accessToken?: string; readonly accessToken?: string;
readonly armToken?: string;
readonly authorizationToken?: string; readonly authorizationToken?: string;
readonly resourceToken?: string; readonly resourceToken?: string;
readonly subscriptionType?: SubscriptionType; readonly subscriptionType?: SubscriptionType;
@@ -104,7 +105,7 @@ export interface UserContext {
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams; readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
readonly feedbackPolicies?: AdminFeedbackPolicySettings; readonly feedbackPolicies?: AdminFeedbackPolicySettings;
readonly dataPlaneRbacEnabled?: boolean; readonly dataPlaneRbacEnabled?: boolean;
readonly refreshCosmosClient?: boolean; readonly hasDataPlaneRbacSettingChanged?: boolean;
} }
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo"; export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";

View File

@@ -93,7 +93,7 @@ export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
}; };
export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [ export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
MongoProxyEndpoints.Development, MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,

View File

@@ -0,0 +1,104 @@
import { MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
import { resetConfigContext, updateConfigContext } from "ConfigContext";
import { DatabaseAccount, IpRule } from "Contracts/DataModels";
import { updateUserContext } from "UserContext";
import { MongoProxyOutboundIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
describe("NetworkUtility tests", () => {
describe("getNetworkSettingsWarningMessage", () => {
const publicAccessMessagePart = "Please enable public access to proceed";
const accessMessagePart = "Please allow access from Azure Portal to proceed";
let warningMessageResult: string;
const warningMessageFunc = (msg: string) => (warningMessageResult = msg);
beforeEach(() => {
warningMessageResult = undefined;
});
afterEach(() => {
resetConfigContext();
});
it("should return no message when publicNetworkAccess is enabled", async () => {
updateUserContext({
databaseAccount: {
properties: {
publicNetworkAccess: "Enabled",
},
} as DatabaseAccount,
});
await getNetworkSettingsWarningMessage(warningMessageFunc);
expect(warningMessageResult).toBeUndefined();
});
it("should return publicAccessMessage when publicNetworkAccess is disabled", async () => {
updateUserContext({
databaseAccount: {
properties: {
publicNetworkAccess: "Disabled",
},
} as DatabaseAccount,
});
await getNetworkSettingsWarningMessage(warningMessageFunc);
expect(warningMessageResult).toContain(publicAccessMessagePart);
});
it(`should return no message when the appropriate ip rules are added to mongo/cassandra account per endpoint`, async () => {
const portalBackendOutboundIPs: string[] = [
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac],
...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod],
];
updateUserContext({
databaseAccount: {
kind: "MongoDB",
properties: {
ipRules: portalBackendOutboundIPs.map((ip: string) => ({ ipAddressOrRange: ip }) as IpRule),
publicNetworkAccess: "Enabled",
},
} as DatabaseAccount,
});
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
});
let asyncWarningMessageResult: string;
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
expect(asyncWarningMessageResult).toBeUndefined();
});
it("should return accessMessage when incorrent ip rule is added to mongo/cassandra account per endpoint", async () => {
updateUserContext({
databaseAccount: {
kind: "MongoDB",
properties: {
ipRules: [{ ipAddressOrRange: "1.1.1.1" }],
publicNetworkAccess: "Enabled",
},
} as DatabaseAccount,
});
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
});
let asyncWarningMessageResult: string;
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
expect(asyncWarningMessageResult).toContain(accessMessagePart);
});
// Postgres and vcore mongo account checks basically pass through to CheckFirewallRules so those
// tests are omitted here and included in CheckFirewallRules.test.ts
});
});

View File

@@ -0,0 +1,99 @@
import { CassandraProxyEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
import { configContext } from "ConfigContext";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import { userContext } from "UserContext";
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
export const getNetworkSettingsWarningMessage = async (
setStateFunc: (warningMessage: string) => void,
): Promise<void> => {
const accountProperties = userContext.databaseAccount?.properties;
const accessMessage =
"The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed.";
const publicAccessMessage =
"The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed.";
if (userContext.apiType === "Postgres") {
checkFirewallRules(
"2022-11-08",
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255",
undefined,
setStateFunc,
accessMessage,
);
return;
} else if (userContext.apiType === "VCoreMongo") {
checkFirewallRules(
"2023-03-01-preview",
(rule) =>
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
undefined,
setStateFunc,
accessMessage,
);
return;
} else if (accountProperties) {
// public network access is disabled
if (
accountProperties.publicNetworkAccess !== "Enabled" &&
accountProperties.publicNetworkAccess !== "SecuredByPerimeter"
) {
setStateFunc(publicAccessMessage);
return;
}
const ipRules = accountProperties.ipRules;
// public network access is NOT set to "All networks"
if (ipRules?.length > 0) {
const isProdOrMpacPortalBackendEndpoint: boolean = [
PortalBackendEndpoints.Mpac,
PortalBackendEndpoints.Prod,
].includes(configContext.PORTAL_BACKEND_ENDPOINT);
const portalBackendOutboundIPs: string[] = isProdOrMpacPortalBackendEndpoint
? [
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
]
: PortalBackendOutboundIPs[configContext.PORTAL_BACKEND_ENDPOINT];
let portalIPs: string[] = [...portalBackendOutboundIPs];
if (userContext.apiType === "Mongo") {
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
configContext.MONGO_PROXY_ENDPOINT,
);
const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
portalIPs = [...portalIPs, ...mongoProxyOutboundIPs];
} else if (userContext.apiType === "Cassandra") {
const isProdOrMpacCassandraProxyEndpoint: boolean = [
CassandraProxyEndpoints.Mpac,
CassandraProxyEndpoints.Prod,
].includes(configContext.CASSANDRA_PROXY_ENDPOINT);
const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint
? [
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac],
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod],
]
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
portalIPs = [...portalIPs, ...cassandraProxyOutboundIPs];
}
let numberOfMatches = 0;
ipRules.forEach((ipRule) => {
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {
numberOfMatches++;
}
});
if (numberOfMatches !== portalIPs.length) {
setStateFunc(accessMessage);
}
}
}
};

View File

@@ -3,6 +3,7 @@ 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";
import { updateUserContext } from "UserContext";
const msalInstance = await getMsalInstance(); const msalInstance = await getMsalInstance();
@@ -79,7 +80,7 @@ export function useAADAuth(): ReturnType {
authority: `${configContext.AAD_ENDPOINT}${tenantId}`, authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
scopes: [`${configContext.ARM_ENDPOINT}/.default`], scopes: [`${configContext.ARM_ENDPOINT}/.default`],
}); });
updateUserContext({ armToken: armToken });
setArmToken(armToken); setArmToken(armToken);
setAuthFailure(null); setAuthFailure(null);
} catch (error) { } catch (error) {

View File

@@ -1,6 +1,8 @@
import { HttpHeaders } from "Common/Constants"; import { HttpHeaders } from "Common/Constants";
import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph"; import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph";
import useSWR from "swr"; import useSWR from "swr";
import { updateUserContext, userContext } from "UserContext";
import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels"; import { DatabaseAccount } from "../Contracts/DataModels";
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -33,12 +35,9 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken:
return accounts.sort((a, b) => a.name.localeCompare(b.name)); return accounts.sort((a, b) => a.name.localeCompare(b.name));
} }
export async function fetchDatabaseAccountsFromGraph( export async function fetchDatabaseAccountsFromGraph(subscriptionId: string): Promise<DatabaseAccount[]> {
subscriptionId: string,
accessToken: string,
): Promise<DatabaseAccount[]> {
const headers = new Headers(); const headers = new Headers();
const bearer = `Bearer ${accessToken}`; const bearer = `Bearer ${userContext.armToken}`;
headers.append("Authorization", bearer); headers.append("Authorization", bearer);
headers.append(HttpHeaders.contentType, "application/json"); headers.append(HttpHeaders.contentType, "application/json");
@@ -46,8 +45,9 @@ export async function fetchDatabaseAccountsFromGraph(
const apiVersion = "2021-03-01"; const apiVersion = "2021-03-01";
const managementResourceGraphAPIURL = `${configContext.ARM_ENDPOINT}providers/Microsoft.ResourceGraph/resources?api-version=${apiVersion}`; const managementResourceGraphAPIURL = `${configContext.ARM_ENDPOINT}providers/Microsoft.ResourceGraph/resources?api-version=${apiVersion}`;
const databaseAccounts: DatabaseAccount[] = []; let databaseAccounts: DatabaseAccount[] = [];
let skipToken: string; let skipToken: string;
console.log("Old ARM Token - fetchDatabaseAccountsFromGraph function", userContext.armToken);
do { do {
const body = { const body = {
query: databaseAccountsQuery, query: databaseAccountsQuery,
@@ -85,10 +85,81 @@ export async function fetchDatabaseAccountsFromGraph(
return databaseAccounts.sort((a, b) => a.name.localeCompare(b.name)); return databaseAccounts.sort((a, b) => a.name.localeCompare(b.name));
} }
export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] | undefined { export function useDatabaseAccounts(subscriptionId: string): DatabaseAccount[] | undefined {
const { data } = useSWR( const { data } = useSWR(
() => (armToken && subscriptionId ? ["databaseAccounts", subscriptionId, armToken] : undefined), () => (subscriptionId ? ["databaseAccounts", subscriptionId] : undefined),
(_, subscriptionId, armToken) => fetchDatabaseAccountsFromGraph(subscriptionId, armToken), (_, subscriptionId) => runCommand(fetchDatabaseAccountsFromGraph, subscriptionId),
); );
return data; return data;
} }
// Define the types for your responses
interface DatabaseAccount {
name: string;
id: string;
// Add other relevant fields as per your use case
}
interface QueryRequestOptions {
$top?: number;
$skipToken?: string;
$allowPartialScopes?: boolean;
}
// Define the configuration context and headers if not already defined
const configContext = {
ARM_ENDPOINT: "https://management.azure.com/",
AAD_ENDPOINT: "https://login.microsoftonline.com/",
};
interface QueryResponse {
data?: any[];
$skipToken?: string;
}
export async function runCommand<T>(fn: (...args: any[]) => Promise<T>, ...args: any[]): Promise<T> {
try {
// Attempt to execute the function passed as an argument
const result = await fn(...args);
console.log("Successfully executed function:", fn.name, result);
return result;
} catch (error) {
// Handle any error that is thrown during the execution of the function
if (error) {
console.log("Creating new token");
const msalInstance = await getMsalInstance();
const cachedAccount = msalInstance.getAllAccounts()?.[0];
const cachedTenantId = localStorage.getItem("cachedTenantId");
msalInstance.setActiveAccount(cachedAccount);
// TODO: Add condition to check if the ARM token needs to be renewed, then we need to run the code below for creating the ARM token
console.log("Creating new ARM token");
const newAccessToken = await acquireTokenWithMsal(msalInstance, {
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
});
updateUserContext({ armToken: newAccessToken });
// TODO: add condition to check if AAD token needs to be renewed (i.e) Token provider has failed with expired AAD token and create a new AAD Token using the below code
// const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(/\/$/, "/.default");
// console.log('Creating new AAD token');
// let aadToken = await acquireTokenWithMsal(msalInstance, {
// forceRefresh: true,
// authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
// scopes: [hrefEndpoint],
// });
// updateUserContext({aadToken: aadToken});
//console.log('Latest AAD Token', fn.name, userContext.aadToken);
const result = await fn(...args);
return result;
} else {
console.error("An error occurred:", error.message);
throw new error();
}
}
}

View File

@@ -7,13 +7,9 @@ import Explorer from "Explorer/Explorer";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import {
AppStateComponentNames,
OPEN_TABS_SUBCOMPONENT_NAME,
readSubComponentState,
} from "Shared/AppStatePersistenceUtility";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils"; import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { logConsoleError } from "Utils/NotificationConsoleUtils"; import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
@@ -84,11 +80,6 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
await updateContextForCopilot(explorer); await updateContextForCopilot(explorer);
await updateContextForSampleData(explorer); await updateContextForSampleData(explorer);
} }
if (userContext.features.restoreTabs) {
restoreOpenTabs();
}
setExplorer(explorer); setExplorer(explorer);
} }
}; };
@@ -141,7 +132,7 @@ async function configureFabric(): Promise<Explorer> {
await scheduleRefreshDatabaseResourceToken(true); await scheduleRefreshDatabaseResourceToken(true);
resolve(explorer); resolve(explorer);
await explorer.refreshAllDatabases(); await explorer.refreshAllDatabases();
if (userContext.fabricContext.isVisible) { if (userContext.fabricContext.isVisible && !firstContainerOpened) {
firstContainerOpened = true; firstContainerOpened = true;
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId); openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
} }
@@ -438,7 +429,6 @@ function createExplorerFabric(params: { connectionId: string; isVisible: boolean
}, },
}, },
}); });
useTabs.getState().closeAllTabs();
const explorer = new Explorer(); const explorer = new Explorer();
return explorer; return explorer;
} }
@@ -741,6 +731,8 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
} }
} }
getNetworkSettingsWarningMessage(useTabs.getState().setNetworkSettingsWarning);
if (inputs.features) { if (inputs.features) {
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features))); Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
} }
@@ -824,17 +816,3 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
interface SampledataconnectionResponse { interface SampledataconnectionResponse {
connectionString: string; connectionString: string;
} }
const restoreOpenTabs = () => {
const openTabsState = readSubComponentState<(DataExplorerAction | undefined)[]>(
AppStateComponentNames.DataExplorerAction,
OPEN_TABS_SUBCOMPONENT_NAME,
undefined,
[],
);
openTabsState.forEach((openTabState) => {
if (openTabState) {
handleOpenAction(openTabState, useDatabases.getState().databases, this);
}
});
};

View File

@@ -3,6 +3,8 @@ import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph
import useSWR from "swr"; import useSWR from "swr";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import { Subscription } from "../Contracts/DataModels"; import { Subscription } from "../Contracts/DataModels";
import { runCommand } from "hooks/useDatabaseAccounts";
import { userContext } from "UserContext";
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
interface SubscriptionListResult { interface SubscriptionListResult {
@@ -35,9 +37,9 @@ export async function fetchSubscriptions(accessToken: string): Promise<Subscript
return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName)); return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName));
} }
export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<Subscription[]> { export async function fetchSubscriptionsFromGraph(): Promise<Subscription[]> {
const headers = new Headers(); const headers = new Headers();
const bearer = `Bearer ${accessToken}`; const bearer = `Bearer ${userContext.armToken}`;
headers.append("Authorization", bearer); headers.append("Authorization", bearer);
headers.append(HttpHeaders.contentType, "application/json"); headers.append(HttpHeaders.contentType, "application/json");
@@ -48,6 +50,7 @@ export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<
const subscriptions: Subscription[] = []; const subscriptions: Subscription[] = [];
let skipToken: string; let skipToken: string;
console.log("Old ARM Token fetchSubscriptionsFromGraph fn", userContext.armToken);
do { do {
const body = { const body = {
query: subscriptionsQuery, query: subscriptionsQuery,
@@ -86,9 +89,14 @@ export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<
} }
export function useSubscriptions(armToken: string): Subscription[] | undefined { export function useSubscriptions(armToken: string): Subscription[] | undefined {
const { data } = useSWR( const { data, error } = useSWR(
() => (armToken ? ["subscriptions", armToken] : undefined), () => (armToken ? ["subscriptions", armToken] : undefined),
(_, armToken) => fetchSubscriptionsFromGraph(armToken), (_) => runCommand(fetchSubscriptionsFromGraph),
); );
if (error) {
console.error("Error fetching subscriptions:", error);
}
return data; return data;
} }

View File

@@ -1,11 +1,5 @@
import { clamp } from "@fluentui/react"; import { clamp } from "@fluentui/react";
import { OpenTab } from "Contracts/ActionContracts";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import {
AppStateComponentNames,
OPEN_TABS_SUBCOMPONENT_NAME,
saveSubComponentState,
} from "Shared/AppStatePersistenceUtility";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels"; import { CollectionTabKind } from "../Contracts/ViewModels";
@@ -18,6 +12,7 @@ export interface TabsState {
openedReactTabs: ReactTabKind[]; openedReactTabs: ReactTabKind[];
activeTab: TabsBase | undefined; activeTab: TabsBase | undefined;
activeReactTab: ReactTabKind | undefined; activeReactTab: ReactTabKind | undefined;
networkSettingsWarning: string;
queryCopilotTabInitialInput: string; queryCopilotTabInitialInput: string;
isTabExecuting: boolean; isTabExecuting: boolean;
isQueryErrorThrown: boolean; isQueryErrorThrown: boolean;
@@ -32,6 +27,7 @@ export interface TabsState {
closeAllNotebookTabs: (hardClose: boolean) => void; closeAllNotebookTabs: (hardClose: boolean) => void;
openAndActivateReactTab: (tabKind: ReactTabKind) => void; openAndActivateReactTab: (tabKind: ReactTabKind) => void;
closeReactTab: (tabKind: ReactTabKind) => void; closeReactTab: (tabKind: ReactTabKind) => void;
setNetworkSettingsWarning: (warningMessage: string) => void;
setQueryCopilotTabInitialInput: (input: string) => void; setQueryCopilotTabInitialInput: (input: string) => void;
setIsTabExecuting: (state: boolean) => void; setIsTabExecuting: (state: boolean) => void;
setIsQueryErrorThrown: (state: boolean) => void; setIsQueryErrorThrown: (state: boolean) => void;
@@ -40,8 +36,6 @@ export interface TabsState {
selectLeftTab: () => void; selectLeftTab: () => void;
selectRightTab: () => void; selectRightTab: () => void;
closeActiveTab: () => void; closeActiveTab: () => void;
closeAllTabs: () => void;
persistTabsState: () => void;
} }
export enum ReactTabKind { export enum ReactTabKind {
@@ -67,6 +61,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [], openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [],
activeTab: undefined, activeTab: undefined,
activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined, activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined,
networkSettingsWarning: "",
queryCopilotTabInitialInput: "", queryCopilotTabInitialInput: "",
isTabExecuting: false, isTabExecuting: false,
isQueryErrorThrown: false, isQueryErrorThrown: false,
@@ -78,9 +73,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
}, },
activateNewTab: (tab: TabsBase): void => { activateNewTab: (tab: TabsBase): void => {
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined })); set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
tab.triggerPersistState = get().persistTabsState;
tab.onActivate(); tab.onActivate();
get().persistTabsState();
}, },
activateReactTab: (tabKind: ReactTabKind): void => { activateReactTab: (tabKind: ReactTabKind): void => {
// Clear the selected node when switching to a react tab. // Clear the selected node when switching to a react tab.
@@ -137,8 +130,6 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
} }
set({ openedTabs: updatedTabs }); set({ openedTabs: updatedTabs });
get().persistTabsState();
}, },
closeAllNotebookTabs: (hardClose): void => { closeAllNotebookTabs: (hardClose): void => {
const isNotebook = (tabKind: CollectionTabKind): boolean => { const isNotebook = (tabKind: CollectionTabKind): boolean => {
@@ -187,6 +178,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
set({ openedReactTabs: updatedOpenedReactTabs }); set({ openedReactTabs: updatedOpenedReactTabs });
}, },
setNetworkSettingsWarning: (warningMessage: string) => set({ networkSettingsWarning: warningMessage }),
setQueryCopilotTabInitialInput: (input: string) => set({ queryCopilotTabInitialInput: input }), setQueryCopilotTabInitialInput: (input: string) => set({ queryCopilotTabInitialInput: input }),
setIsTabExecuting: (state: boolean) => { setIsTabExecuting: (state: boolean) => {
set({ isTabExecuting: state }); set({ isTabExecuting: state });
@@ -234,18 +226,4 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
state.closeTab(state.activeTab); state.closeTab(state.activeTab);
} }
}, },
closeAllTabs: () => {
set({ openedTabs: [], openedReactTabs: [], activeTab: undefined, activeReactTab: undefined });
},
persistTabsState: () => {
const state = get();
const openTabsStates = state.openedTabs.map((tab) => tab.getPersistedState());
saveSubComponentState<OpenTab[]>(
AppStateComponentNames.DataExplorerAction,
OPEN_TABS_SUBCOMPONENT_NAME,
undefined,
openTabsStates,
);
},
})); }));

View File

@@ -1,11 +1,11 @@
// import { initializeIcons } from "@fluentui/react"; import { initializeIcons } from "@fluentui/react";
import { configure } from "enzyme"; import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16"; import Adapter from "enzyme-adapter-react-16";
import "jest-canvas-mock"; import "jest-canvas-mock";
import enableHooks from "jest-react-hooks-shallow"; import enableHooks from "jest-react-hooks-shallow";
import { TextDecoder, TextEncoder } from "util"; import { TextDecoder, TextEncoder } from "util";
configure({ adapter: new Adapter() }); configure({ adapter: new Adapter() });
// initializeIcons(); initializeIcons();
if (typeof window.URL.createObjectURL === "undefined") { if (typeof window.URL.createObjectURL === "undefined") {
Object.defineProperty(window.URL, "createObjectURL", { value: () => {} }); Object.defineProperty(window.URL, "createObjectURL", { value: () => {} });

View File

@@ -193,12 +193,7 @@ module.exports = function (_env = {}, argv = {}) {
]; ];
if (argv.analyze) { if (argv.analyze) {
plugins.push( plugins.push(new BundleAnalyzerPlugin());
new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "bundle-report.html",
}),
);
} }
return { return {
@@ -261,17 +256,6 @@ module.exports = function (_env = {}, argv = {}) {
}, },
}), }),
], ],
splitChunks: {
chunks: "all",
cacheGroups: {
fluentIcons: {
test: /[\\/]node_modules[\\/]@fluentui[\\/](font-icons-mdl2|react-icons)/,
name: "fluent-icons",
chunks: "all",
enforce: true,
},
},
},
}, },
watch: false, watch: false,
// Hack since it is hard to disable watch entirely with webpack dev server https://github.com/webpack/webpack-dev-server/issues/1251#issuecomment-654240734 // Hack since it is hard to disable watch entirely with webpack dev server https://github.com/webpack/webpack-dev-server/issues/1251#issuecomment-654240734