mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-06 03:00:23 +00:00
Compare commits
18 Commits
users/sevo
...
ashleyst/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4facfba0c1 | ||
|
|
7e95f5d8c8 | ||
|
|
1be221e106 | ||
|
|
8e7a3db67e | ||
|
|
07c0ead523 | ||
|
|
4296b5ae02 | ||
|
|
e8a5658799 | ||
|
|
b4973e8367 | ||
|
|
4b207f3fa6 | ||
|
|
c5b7f599b3 | ||
|
|
6aeac542b1 | ||
|
|
0d22d4ab4d | ||
|
|
0658448b54 | ||
|
|
833d677d20 | ||
|
|
038142c180 | ||
|
|
94d3fcb30f | ||
|
|
d3722f2c99 | ||
|
|
5a5e155205 |
@@ -1906,8 +1906,14 @@ input::-webkit-calendar-picker-indicator::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 5px;
|
height: 32px;
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navTabHeight {
|
.navTabHeight {
|
||||||
|
|||||||
56
package-lock.json
generated
56
package-lock.json
generated
@@ -2527,13 +2527,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": {
|
"node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": {
|
||||||
"version": "0.10.4",
|
"version": "0.10.6",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
|
||||||
"integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==",
|
"integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-define-polyfill-provider": "^0.6.1",
|
"@babel/helper-define-polyfill-provider": "^0.6.2",
|
||||||
"core-js-compat": "^3.36.1"
|
"core-js-compat": "^3.38.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
@@ -2932,10 +2932,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/core": {
|
"node_modules/@floating-ui/core": {
|
||||||
"version": "1.6.3",
|
"version": "1.6.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/utils": "^0.2.3"
|
"@floating-ui/utils": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/devtools": {
|
"node_modules/@floating-ui/devtools": {
|
||||||
@@ -2945,15 +2945,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/dom": {
|
"node_modules/@floating-ui/dom": {
|
||||||
"version": "1.6.6",
|
"version": "1.6.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/core": "^1.0.0",
|
"@floating-ui/core": "^1.0.0",
|
||||||
"@floating-ui/utils": "^0.2.3"
|
"@floating-ui/utils": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/utils": {
|
"node_modules/@floating-ui/utils": {
|
||||||
"version": "0.2.3",
|
"version": "0.2.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@fluentui/date-time-utilities": {
|
"node_modules/@fluentui/date-time-utilities": {
|
||||||
@@ -3501,7 +3501,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.8.10.tgz",
|
"resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.8.10.tgz",
|
||||||
"integrity": "sha512-Xvnn6uKMsinMg/zo79KBNCDABnl0gpmArQYNQya9FCNRzvmHUCDvuQCqv4IKslvPvuC0Ya8mR2NORm2w0JoZiw==",
|
"integrity": "sha512-Xvnn6uKMsinMg/zo79KBNCDABnl0gpmArQYNQya9FCNRzvmHUCDvuQCqv4IKslvPvuC0Ya8mR2NORm2w0JoZiw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-window-provider": "^2.2.27",
|
"@fluentui/react-window-provider": "^2.2.28",
|
||||||
"@fluentui/set-version": "^8.2.23",
|
"@fluentui/set-version": "^8.2.23",
|
||||||
"@fluentui/utilities": "^8.15.13",
|
"@fluentui/utilities": "^8.15.13",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
@@ -4426,9 +4426,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fluentui/react-window-provider": {
|
"node_modules/@fluentui/react-window-provider": {
|
||||||
"version": "2.2.27",
|
"version": "2.2.28",
|
||||||
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.27.tgz",
|
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.28.tgz",
|
||||||
"integrity": "sha512-Dg0G9bizjryV0Q/r0CPtCVTPa2II/EsT9E6JT3jPSALjQADDLlW4/+ZXbcEC7geZ/40+KpZDmhplvk/AJSFBKg==",
|
"integrity": "sha512-YdZ74HTaoDwlvLDzoBST80/17ExIl93tLJpTxnqK5jlJOAUVQ+mxLPF2HQEJq+SZr5IMXHsQ56w/KaZVRn72YA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/set-version": "^8.2.23",
|
"@fluentui/set-version": "^8.2.23",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
@@ -4512,7 +4512,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/dom-utilities": "^2.3.7",
|
"@fluentui/dom-utilities": "^2.3.7",
|
||||||
"@fluentui/merge-styles": "^8.6.12",
|
"@fluentui/merge-styles": "^8.6.12",
|
||||||
"@fluentui/react-window-provider": "^2.2.27",
|
"@fluentui/react-window-provider": "^2.2.28",
|
||||||
"@fluentui/set-version": "^8.2.23",
|
"@fluentui/set-version": "^8.2.23",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -14966,9 +14966,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.23.2",
|
"version": "4.23.3",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
|
||||||
"integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==",
|
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -14984,9 +14984,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001640",
|
"caniuse-lite": "^1.0.30001646",
|
||||||
"electron-to-chromium": "^1.4.820",
|
"electron-to-chromium": "^1.5.4",
|
||||||
"node-releases": "^2.0.14",
|
"node-releases": "^2.0.18",
|
||||||
"update-browserslist-db": "^1.1.0"
|
"update-browserslist-db": "^1.1.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -15142,9 +15142,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001645",
|
"version": "1.0.30001651",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001645.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
|
||||||
"integrity": "sha512-GFtY2+qt91kzyMk6j48dJcwJVq5uTkk71XxE3RtScx7XWRLsO7bU44LOFkOZYR8w9YMS0UhPSYpN/6rAMImmLw==",
|
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -16063,12 +16063,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.37.1",
|
"version": "3.38.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz",
|
||||||
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
|
"integrity": "sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.23.0"
|
"browserslist": "^4.23.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ export class BackendApi {
|
|||||||
public static readonly GenerateToken: string = "GenerateToken";
|
public static readonly GenerateToken: string = "GenerateToken";
|
||||||
public static readonly PortalSettings: string = "PortalSettings";
|
public static readonly PortalSettings: string = "PortalSettings";
|
||||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
||||||
|
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
||||||
|
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
export class PortalBackendEndpoints {
|
||||||
@@ -183,6 +185,12 @@ export class CassandraProxyAPIs {
|
|||||||
public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema";
|
public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AadEndpoints {
|
||||||
|
public static readonly Prod: string = "https://login.microsoftonline.com/";
|
||||||
|
public static readonly Fairfax: string = "https://login.microsoftonline.us/";
|
||||||
|
public static readonly Mooncake: string = "https://login.partner.microsoftonline.cn/";
|
||||||
|
}
|
||||||
|
|
||||||
export class Queries {
|
export class Queries {
|
||||||
public static CustomPageOption: string = "custom";
|
public static CustomPageOption: string = "custom";
|
||||||
public static UnlimitedPageOption: string = "unlimited";
|
public static UnlimitedPageOption: string = "unlimited";
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizatio
|
|||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||||
|
import * as Logger from "../Common/Logger";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
import { getErrorMessage } from "./ErrorHandlingUtils";
|
import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||||
import * as Logger from "../Common/Logger";
|
|
||||||
|
|
||||||
const _global = typeof self === "undefined" ? window : self;
|
const _global = typeof self === "undefined" ? window : self;
|
||||||
|
|
||||||
@@ -123,6 +124,37 @@ export async function getTokenFromAuthService(
|
|||||||
verb: string,
|
verb: string,
|
||||||
resourceType: string,
|
resourceType: string,
|
||||||
resourceId?: string,
|
resourceId?: string,
|
||||||
|
): Promise<AuthorizationToken> {
|
||||||
|
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
||||||
|
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||||
|
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-ms-encrypted-auth-token": userContext.accessToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
verb,
|
||||||
|
resourceType,
|
||||||
|
resourceId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const result: AuthorizationToken = await response.json();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTokenFromAuthService_ToBeDeprecated(
|
||||||
|
verb: string,
|
||||||
|
resourceType: string,
|
||||||
|
resourceId?: string,
|
||||||
): Promise<AuthorizationToken> {
|
): Promise<AuthorizationToken> {
|
||||||
try {
|
try {
|
||||||
const host = configContext.BACKEND_ENDPOINT;
|
const host = configContext.BACKEND_ENDPOINT;
|
||||||
|
|||||||
@@ -550,6 +550,49 @@ export function deleteDocument_ToBeDeprecated(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteDocuments(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
documentIds: DocumentId[],
|
||||||
|
): Promise<{
|
||||||
|
deletedCount: number;
|
||||||
|
isAcknowledged: boolean;
|
||||||
|
}> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
|
||||||
|
const rids = documentIds.map((documentId) => documentId.id());
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
databaseID: databaseId,
|
||||||
|
collectionID: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}`,
|
||||||
|
resourceIDs: rids,
|
||||||
|
subscriptionID: userContext.subscriptionId,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
databaseAccountName: databaseAccount.name,
|
||||||
|
};
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("bulkdelete");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}/bulkdelete`, {
|
||||||
|
method: "DELETE",
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "deleting documents", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
@@ -677,18 +720,11 @@ export function useMongoProxyEndpoint(api: string): boolean {
|
|||||||
MongoProxyEndpoints.Local,
|
MongoProxyEndpoints.Local,
|
||||||
MongoProxyEndpoints.Mpac,
|
MongoProxyEndpoints.Mpac,
|
||||||
MongoProxyEndpoints.Prod,
|
MongoProxyEndpoints.Prod,
|
||||||
// MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
MongoProxyEndpoints.Mooncake,
|
||||||
];
|
];
|
||||||
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
|
||||||
if (
|
|
||||||
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local &&
|
|
||||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
|
||||||
) {
|
|
||||||
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
canAccessMongoProxy &&
|
|
||||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||||
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import * as React from "react";
|
|||||||
|
|
||||||
export interface TooltipProps {
|
export interface TooltipProps {
|
||||||
children: string;
|
children: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }: TooltipProps) => {
|
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children, className }: TooltipProps) => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className={className}>
|
||||||
<TooltipHost content={children}>
|
<TooltipHost content={children}>
|
||||||
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
|
|||||||
@@ -53,10 +53,8 @@ export interface ConfigContext {
|
|||||||
NEW_BACKEND_APIS?: BackendApi[];
|
NEW_BACKEND_APIS?: BackendApi[];
|
||||||
MONGO_BACKEND_ENDPOINT?: string;
|
MONGO_BACKEND_ENDPOINT?: string;
|
||||||
MONGO_PROXY_ENDPOINT?: string;
|
MONGO_PROXY_ENDPOINT?: string;
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean;
|
|
||||||
NEW_MONGO_APIS?: string[];
|
NEW_MONGO_APIS?: string[];
|
||||||
CASSANDRA_PROXY_ENDPOINT?: string;
|
CASSANDRA_PROXY_ENDPOINT?: string;
|
||||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: boolean;
|
|
||||||
NEW_CASSANDRA_APIS?: string[];
|
NEW_CASSANDRA_APIS?: string[];
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
@@ -87,7 +85,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||||
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`,
|
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net$`,
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
@@ -117,11 +115,10 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
"deleteDocument",
|
"deleteDocument",
|
||||||
"createCollectionWithProxy",
|
"createCollectionWithProxy",
|
||||||
"legacyMongoShell",
|
"legacyMongoShell",
|
||||||
|
"bulkdelete",
|
||||||
],
|
],
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const useTreeStyles = makeStyles({
|
|||||||
minWidth: "100%",
|
minWidth: "100%",
|
||||||
rowGap: "0px",
|
rowGap: "0px",
|
||||||
paddingTop: "0px",
|
paddingTop: "0px",
|
||||||
[treeIconWidth]: "20px",
|
[treeIconWidth]: "16px",
|
||||||
[leafNodeSpacing]: "24px",
|
[leafNodeSpacing]: "24px",
|
||||||
},
|
},
|
||||||
nodeIcon: {
|
nodeIcon: {
|
||||||
@@ -25,12 +25,13 @@ export const useTreeStyles = makeStyles({
|
|||||||
height: `var(${treeIconWidth})`,
|
height: `var(${treeIconWidth})`,
|
||||||
},
|
},
|
||||||
treeItem: {},
|
treeItem: {},
|
||||||
nodeLabel: {},
|
nodeLabel: {
|
||||||
|
whiteSpace: "nowrap", // Don't wrap text, there will be a scrollbar.
|
||||||
|
},
|
||||||
treeItemLayout: {
|
treeItemLayout: {
|
||||||
fontSize: tokens.fontSizeBase300,
|
fontSize: tokens.fontSizeBase300,
|
||||||
height: tokens.layoutRowHeight,
|
height: tokens.layoutRowHeight,
|
||||||
...cosmosShorthands.borderBottom(),
|
...cosmosShorthands.borderBottom(),
|
||||||
paddingLeft: `calc(var(${treeItemLevelToken}, 1) * ${tokens.spacingHorizontalXXL})`,
|
|
||||||
|
|
||||||
// Some sneaky CSS variables stuff to change the background color of the action button on hover.
|
// Some sneaky CSS variables stuff to change the background color of the action button on hover.
|
||||||
[actionButtonBackground]: tokens.colorNeutralBackground1,
|
[actionButtonBackground]: tokens.colorNeutralBackground1,
|
||||||
|
|||||||
@@ -149,15 +149,16 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
|
|
||||||
// We use the expandIcon slot to hold the node icon too.
|
// We use the expandIcon slot to hold the node icon too.
|
||||||
// We only show a node icon for leaf nodes, even if a branch node has an iconSrc.
|
// We only show a node icon for leaf nodes, even if a branch node has an iconSrc.
|
||||||
const expandIcon = isLoading ? (
|
const treeIcon =
|
||||||
<Spinner size="extra-tiny" />
|
node.iconSrc === undefined ? undefined : typeof node.iconSrc === "string" ? (
|
||||||
) : !isBranch ? (
|
|
||||||
typeof node.iconSrc === "string" ? (
|
|
||||||
<img src={node.iconSrc} className={treeStyles.nodeIcon} alt="" />
|
<img src={node.iconSrc} className={treeStyles.nodeIcon} alt="" />
|
||||||
) : (
|
) : (
|
||||||
node.iconSrc
|
node.iconSrc
|
||||||
)
|
);
|
||||||
) : openItems.includes(treeNodeId) ? (
|
|
||||||
|
const expandIcon = isLoading ? (
|
||||||
|
<Spinner size="extra-tiny" />
|
||||||
|
) : !isBranch ? undefined : openItems.includes(treeNodeId) ? (
|
||||||
<ChevronDown20Regular data-test="TreeNode/CollapseIcon" />
|
<ChevronDown20Regular data-test="TreeNode/CollapseIcon" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight20Regular data-text="TreeNode/ExpandIcon" />
|
<ChevronRight20Regular data-text="TreeNode/ExpandIcon" />
|
||||||
@@ -174,7 +175,6 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
className={mergeClasses(
|
className={mergeClasses(
|
||||||
treeStyles.treeItemLayout,
|
treeStyles.treeItemLayout,
|
||||||
expandIcon ? undefined : treeStyles.treeItemLayoutNoIcon,
|
|
||||||
shouldShowAsSelected && treeStyles.selectedItem,
|
shouldShowAsSelected && treeStyles.selectedItem,
|
||||||
node.className && treeStyles[node.className],
|
node.className && treeStyles[node.className],
|
||||||
)}
|
)}
|
||||||
@@ -200,6 +200,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
iconBefore={treeIcon}
|
||||||
expandIcon={expandIcon}
|
expandIcon={expandIcon}
|
||||||
>
|
>
|
||||||
<span className={treeStyles.nodeLabel}>{node.label}</span>
|
<span className={treeStyles.nodeLabel}>{node.label}</span>
|
||||||
|
|||||||
@@ -10,16 +10,23 @@ exports[`TreeNodeComponent does not render children if the node is loading 1`] =
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
iconBefore={
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="rootIcon"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -156,7 +163,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "branch",
|
"itemType": "branch",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -179,11 +186,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="rootIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -208,7 +225,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -231,18 +248,28 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="rootIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-Tree rnv2ez3 ___jy13a00_lpffjy0 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
class="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
role="tree"
|
role="tree"
|
||||||
>
|
>
|
||||||
@@ -256,7 +283,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -279,11 +306,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child1Icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child1Label
|
child1Label
|
||||||
</span>
|
</span>
|
||||||
@@ -300,7 +337,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -323,11 +360,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child2LoadingIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child2LoadingLabel
|
child2LoadingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -343,7 +390,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -363,11 +410,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child3ExpandingIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child3ExpandingLabel
|
child3ExpandingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -383,16 +440,23 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
iconBefore={
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="rootIcon"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -419,11 +483,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
</ChevronRight20Regular>
|
</ChevronRight20Regular>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden={true}
|
||||||
|
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="rootIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout__main rklbe47"
|
className="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -431,7 +505,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
>
|
>
|
||||||
<TreeProvider
|
<TreeProvider
|
||||||
@@ -499,7 +573,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-Tree rnv2ez3 ___jy13a00_lpffjy0 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
className="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
role="tree"
|
role="tree"
|
||||||
>
|
>
|
||||||
@@ -587,7 +661,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "branch",
|
"itemType": "branch",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -610,11 +684,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child1Icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child1Label
|
child1Label
|
||||||
</span>
|
</span>
|
||||||
@@ -639,7 +723,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -662,11 +746,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child1Icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child1Label
|
child1Label
|
||||||
</span>
|
</span>
|
||||||
@@ -680,16 +774,23 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
iconBefore={
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child1Icon"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -716,11 +817,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
</ChevronRight20Regular>
|
</ChevronRight20Regular>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden={true}
|
||||||
|
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child1Icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout__main rklbe47"
|
className="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child1Label
|
child1Label
|
||||||
</span>
|
</span>
|
||||||
@@ -728,7 +839,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
||||||
data-test="Tree:root/child1Label"
|
data-test="Tree:root/child1Label"
|
||||||
>
|
>
|
||||||
<TreeProvider
|
<TreeProvider
|
||||||
@@ -821,7 +932,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "branch",
|
"itemType": "branch",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -844,11 +955,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child2LoadingIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child2LoadingLabel
|
child2LoadingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -873,7 +994,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -896,11 +1017,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child2LoadingIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child2LoadingLabel
|
child2LoadingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -914,16 +1045,23 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
iconBefore={
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child2LoadingIcon"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -950,11 +1088,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
</ChevronRight20Regular>
|
</ChevronRight20Regular>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden={true}
|
||||||
|
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child2LoadingIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout__main rklbe47"
|
className="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child2LoadingLabel
|
child2LoadingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1039,7 +1187,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "leaf",
|
"itemType": "leaf",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1059,11 +1207,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child3ExpandingIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child3ExpandingLabel
|
child3ExpandingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1087,7 +1245,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1107,11 +1265,21 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="child3ExpandingIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout__main rklbe47"
|
class="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=""
|
class="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child3ExpandingLabel
|
child3ExpandingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1125,9 +1293,9 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
expandIcon={
|
iconBefore={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1136,12 +1304,12 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="fui-TreeItemLayout__expandIcon rh4pu5o"
|
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
@@ -1153,7 +1321,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
className="fui-TreeItemLayout__main rklbe47"
|
className="fui-TreeItemLayout__main rklbe47"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
child3ExpandingLabel
|
child3ExpandingLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1184,9 +1352,9 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
iconBefore={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1195,7 +1363,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1213,16 +1381,23 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<Spinner
|
<Spinner
|
||||||
size="extra-tiny"
|
size="extra-tiny"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
iconBefore={
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="rootIcon"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1240,16 +1415,23 @@ exports[`TreeNodeComponent renders a node as expandable if it has empty, but def
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
iconBefore={
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="rootIcon"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1313,9 +1495,9 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
"className": "___1r8p62d_0000000 f1xg1ack f1e31b4d",
|
"className": "___1r8p62d_0000000 f1xg1ack f1e31b4d",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
iconBefore={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1324,7 +1506,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1363,9 +1545,9 @@ exports[`TreeNodeComponent renders a single node 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
iconBefore={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1374,7 +1556,7 @@ exports[`TreeNodeComponent renders a single node 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1392,9 +1574,9 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
iconBefore={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1403,7 +1585,7 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
@@ -1421,22 +1603,29 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___kqkdor0_ihxn0o0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
iconBefore={
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="rootIcon"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
>
|
>
|
||||||
<TreeNodeComponent
|
<TreeNodeComponent
|
||||||
@@ -1497,22 +1686,29 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<ChevronRight20Regular
|
<ChevronRight20Regular
|
||||||
data-text="TreeNode/ExpandIcon"
|
data-text="TreeNode/ExpandIcon"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
iconBefore={
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
src="rootIcon"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
>
|
>
|
||||||
<TreeNodeComponent
|
<TreeNodeComponent
|
||||||
@@ -1574,9 +1770,9 @@ exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___kqkdor0_ihxn0o0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
expandIcon={
|
iconBefore={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1585,7 +1781,7 @@ exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className=""
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
>
|
>
|
||||||
rootLabel
|
rootLabel
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1119,7 +1119,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openUploadItemsPanePane(): void {
|
public openUploadItemsPane(): void {
|
||||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
||||||
}
|
}
|
||||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||||
|
|||||||
@@ -167,22 +167,18 @@ export function createContextCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] =
|
const buttons: CommandButtonComponentProps[] = [
|
||||||
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
{
|
||||||
? []
|
iconSrc: SettingsIcon,
|
||||||
: [
|
iconAlt: "Settings",
|
||||||
{
|
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
||||||
iconSrc: SettingsIcon,
|
commandButtonLabel: undefined,
|
||||||
iconAlt: "Settings",
|
ariaLabel: "Settings",
|
||||||
onCommandClick: () =>
|
tooltipText: "Settings",
|
||||||
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
hasPopup: true,
|
||||||
commandButtonLabel: undefined,
|
disabled: false,
|
||||||
ariaLabel: "Settings",
|
},
|
||||||
tooltipText: "Settings",
|
];
|
||||||
hasPopup: true,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const showOpenFullScreen =
|
const showOpenFullScreen =
|
||||||
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
@@ -127,7 +128,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||||
? databaseAccount?.location
|
? databaseAccount?.location
|
||||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||||
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
const disallowedLocationsUri: string = useNewPortalBackendEndpoint(Constants.BackendApi.DisallowedLocations)
|
||||||
|
? `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`
|
||||||
|
: `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(disallowedLocationsUri, {
|
const response = await fetch(disallowedLocationsUri, {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,148 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxOnChangeData,
|
||||||
|
InputOnChangeData,
|
||||||
|
makeStyles,
|
||||||
|
SearchBox,
|
||||||
|
SearchBoxChangeEvent,
|
||||||
|
Text,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { configContext } from "ConfigContext";
|
||||||
|
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||||
|
import { CosmosFluentProvider, getPlatformTheme } from "Explorer/Theme/ThemeUtil";
|
||||||
|
import React from "react";
|
||||||
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
|
|
||||||
|
const useColumnSelectionStyles = makeStyles({
|
||||||
|
paneContainer: {
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
searchBox: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
checkboxContainer: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
checkboxLabel: {
|
||||||
|
padding: "4px 8px",
|
||||||
|
marginBottom: "0px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export interface TableColumnSelectionPaneProps {
|
||||||
|
columnDefinitions: ColumnDefinition[];
|
||||||
|
selectedColumnIds: string[];
|
||||||
|
onSelectionChange: (newSelectedColumnIds: string[]) => void;
|
||||||
|
defaultSelection: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> = ({
|
||||||
|
columnDefinitions,
|
||||||
|
selectedColumnIds,
|
||||||
|
onSelectionChange,
|
||||||
|
defaultSelection,
|
||||||
|
}: TableColumnSelectionPaneProps): JSX.Element => {
|
||||||
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
|
const originalSelectedColumnIds = React.useMemo(() => selectedColumnIds, []);
|
||||||
|
const [columnSearchText, setColumnSearchText] = React.useState<string>("");
|
||||||
|
const [newSelectedColumnIds, setNewSelectedColumnIds] = React.useState<string[]>(originalSelectedColumnIds);
|
||||||
|
const styles = useColumnSelectionStyles();
|
||||||
|
|
||||||
|
const selectedColumnIdsSet = new Set(newSelectedColumnIds);
|
||||||
|
const onCheckedValueChange = (id: string, checkedData?: CheckboxOnChangeData): void => {
|
||||||
|
const checked = checkedData?.checked;
|
||||||
|
if (checked === "mixed" || checked === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
selectedColumnIdsSet.add(id);
|
||||||
|
} else {
|
||||||
|
if (selectedColumnIdsSet.size === 1 && selectedColumnIdsSet.has(id)) {
|
||||||
|
// Don't allow unchecking the last column
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedColumnIdsSet.delete(id);
|
||||||
|
}
|
||||||
|
setNewSelectedColumnIds([...selectedColumnIdsSet]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSave = (): void => {
|
||||||
|
onSelectionChange(newSelectedColumnIds);
|
||||||
|
closeSidePanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearchChange: (event: SearchBoxChangeEvent, data: InputOnChangeData) => void = (_, data) =>
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
setColumnSearchText(data.value);
|
||||||
|
|
||||||
|
const theme = getPlatformTheme(configContext.platform);
|
||||||
|
|
||||||
|
// Filter and move partition keys to the top
|
||||||
|
const columnDefinitionList = columnDefinitions
|
||||||
|
.filter((def) => !columnSearchText || def.label.toLowerCase().includes(columnSearchText.toLowerCase()))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const ID = "id";
|
||||||
|
// "id" always at the top, then partition keys, then everything else sorted
|
||||||
|
if (a.id === ID) {
|
||||||
|
return b.id === ID ? 0 : -1;
|
||||||
|
} else if (b.id === ID) {
|
||||||
|
return a.id === ID ? 0 : 1;
|
||||||
|
} else if (a.isPartitionKey && !b.isPartitionKey) {
|
||||||
|
return -1;
|
||||||
|
} else if (b.isPartitionKey && !a.isPartitionKey) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return a.label.localeCompare(b.label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.paneContainer}>
|
||||||
|
<CosmosFluentProvider>
|
||||||
|
<div className="panelFormWrapper">
|
||||||
|
<div className="panelMainContent" style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
<Text>Select which columns to display in your view of items in your container.</Text>
|
||||||
|
<div /* Wrap <SearchBox> to avoid margin-bottom set by panelMainContent css */>
|
||||||
|
<SearchBox
|
||||||
|
className={styles.searchBox}
|
||||||
|
value={columnSearchText}
|
||||||
|
onChange={onSearchChange}
|
||||||
|
placeholder="Search fields"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.checkboxContainer}>
|
||||||
|
{columnDefinitionList.map((columnDefinition) => (
|
||||||
|
<Checkbox
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
key={columnDefinition.id}
|
||||||
|
label={{
|
||||||
|
className: styles.checkboxLabel,
|
||||||
|
children: `${columnDefinition.label}${columnDefinition.isPartitionKey ? " (partition key)" : ""}`,
|
||||||
|
}}
|
||||||
|
checked={selectedColumnIdsSet.has(columnDefinition.id)}
|
||||||
|
onChange={(_, data) => onCheckedValueChange(columnDefinition.id, data)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button appearance="secondary" size="small" onClick={() => setNewSelectedColumnIds(defaultSelection)}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="panelFooter" style={{ display: "flex", gap: theme.spacingHorizontalS }}>
|
||||||
|
<Button appearance="primary" onClick={onSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button appearance="secondary" onClick={closeSidePanel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CosmosFluentProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Menu,
|
Menu,
|
||||||
|
MenuButton,
|
||||||
MenuButtonProps,
|
MenuButtonProps,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
@@ -25,7 +26,7 @@ import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
|||||||
import { Allotment, AllotmentHandle } from "allotment";
|
import { Allotment, AllotmentHandle } from "allotment";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
const useSidebarStyles = makeStyles({
|
const useSidebarStyles = makeStyles({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
@@ -60,6 +61,7 @@ const useSidebarStyles = makeStyles({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyItems: "center",
|
justifyItems: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
containerType: "size", // Use this container for "@container" queries below this.
|
||||||
...cosmosShorthands.borderBottom(),
|
...cosmosShorthands.borderBottom(),
|
||||||
},
|
},
|
||||||
loadingProgressBar: {
|
loadingProgressBar: {
|
||||||
@@ -83,6 +85,18 @@ const useSidebarStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
globalCommandsMenuButton: {
|
||||||
|
display: "inline-flex",
|
||||||
|
"@container (min-width: 250px)": {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globalCommandsSplitButton: {
|
||||||
|
display: "none",
|
||||||
|
"@container (min-width: 250px)": {
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface GlobalCommandsProps {
|
interface GlobalCommandsProps {
|
||||||
@@ -99,6 +113,12 @@ interface GlobalCommand {
|
|||||||
|
|
||||||
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||||
const styles = useSidebarStyles();
|
const styles = useSidebarStyles();
|
||||||
|
|
||||||
|
// Since we have two buttons in the DOM (one for small screens and one for larger screens), we wrap the entire thing in a div.
|
||||||
|
// However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu.
|
||||||
|
// We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render.
|
||||||
|
const [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
const actions = useMemo<GlobalCommand[]>(() => {
|
const actions = useMemo<GlobalCommand[]>(() => {
|
||||||
if (
|
if (
|
||||||
configContext.platform === Platform.Fabric ||
|
configContext.platform === Platform.Fabric ||
|
||||||
@@ -168,16 +188,22 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
{primaryAction.label}
|
{primaryAction.label}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Menu positioning="below-end">
|
<Menu positioning={{ target: globalCommandButton, position: "below", align: "end" }}>
|
||||||
<MenuTrigger disableButtonEnhancement>
|
<MenuTrigger disableButtonEnhancement>
|
||||||
{(triggerProps: MenuButtonProps) => (
|
{(triggerProps: MenuButtonProps) => (
|
||||||
<SplitButton
|
<div ref={setGlobalCommandButton}>
|
||||||
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
<SplitButton
|
||||||
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
||||||
icon={primaryAction.icon}
|
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
||||||
>
|
className={styles.globalCommandsSplitButton}
|
||||||
{primaryAction.label}
|
icon={primaryAction.icon}
|
||||||
</SplitButton>
|
>
|
||||||
|
{primaryAction.label}
|
||||||
|
</SplitButton>
|
||||||
|
<MenuButton {...triggerProps} icon={primaryAction.icon} className={styles.globalCommandsMenuButton}>
|
||||||
|
New...
|
||||||
|
</MenuButton>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
<MenuPopover>
|
<MenuPopover>
|
||||||
@@ -199,7 +225,7 @@ interface SidebarProps {
|
|||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollapseThreshold = 50;
|
const CollapseThreshold = 140;
|
||||||
|
|
||||||
export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||||
const styles = useSidebarStyles();
|
const styles = useSidebarStyles();
|
||||||
@@ -260,7 +286,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
{/* Collections Tree - Start */}
|
{/* Collections Tree - Start */}
|
||||||
{hasSidebar && (
|
{hasSidebar && (
|
||||||
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
||||||
<Allotment.Pane minSize={24} preferredSize={300}>
|
<Allotment.Pane minSize={24} preferredSize={250}>
|
||||||
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
||||||
<div className={styles.sidebarContainer}>
|
<div className={styles.sidebarContainer}>
|
||||||
{loading && (
|
{loading && (
|
||||||
@@ -314,7 +340,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
)}
|
)}
|
||||||
<Allotment.Pane minSize={800}>
|
<Allotment.Pane minSize={200}>
|
||||||
<Tabs explorer={explorer} />
|
<Tabs explorer={explorer} />
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
</Allotment>
|
</Allotment>
|
||||||
|
|||||||
@@ -753,17 +753,11 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
CassandraProxyEndpoints.Development,
|
CassandraProxyEndpoints.Development,
|
||||||
CassandraProxyEndpoints.Mpac,
|
CassandraProxyEndpoints.Mpac,
|
||||||
CassandraProxyEndpoints.Prod,
|
CassandraProxyEndpoints.Prod,
|
||||||
|
CassandraProxyEndpoints.Fairfax,
|
||||||
|
CassandraProxyEndpoints.Mooncake,
|
||||||
];
|
];
|
||||||
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
|
||||||
if (
|
|
||||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
|
|
||||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
|
||||||
) {
|
|
||||||
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
canAccessCassandraProxy &&
|
|
||||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||||
);
|
);
|
||||||
|
|||||||
106
src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts
Normal file
106
src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// Definitions of State data
|
||||||
|
|
||||||
|
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||||
|
import { deleteState, loadState, saveState, saveStateDebounced } from "Shared/AppStatePersistenceUtility";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
|
const componentName = "DocumentsTab";
|
||||||
|
export enum SubComponentName {
|
||||||
|
ColumnSizes = "ColumnSizes",
|
||||||
|
FilterHistory = "FilterHistory",
|
||||||
|
MainTabDivider = "MainTabDivider",
|
||||||
|
ColumnsSelection = "ColumnsSelection",
|
||||||
|
ColumnSort = "ColumnSort",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
||||||
|
export type FilterHistory = string[];
|
||||||
|
export type WidthDefinition = { widthPx: number };
|
||||||
|
export type TabDivider = { leftPaneWidthPercent: number };
|
||||||
|
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
||||||
|
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param subComponentName
|
||||||
|
* @param collection
|
||||||
|
* @param defaultValue Will be returned if persisted state is not found
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const readSubComponentState = <T>(
|
||||||
|
subComponentName: SubComponentName,
|
||||||
|
collection: ViewModels.CollectionBase,
|
||||||
|
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.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,
|
||||||
|
collection: ViewModels.CollectionBase,
|
||||||
|
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.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(),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
SAVE_BUTTON_ID,
|
SAVE_BUTTON_ID,
|
||||||
UPDATE_BUTTON_ID,
|
UPDATE_BUTTON_ID,
|
||||||
UPLOAD_BUTTON_ID,
|
UPLOAD_BUTTON_ID,
|
||||||
|
addStringsNoDuplicate,
|
||||||
buildQuery,
|
buildQuery,
|
||||||
getDiscardExistingDocumentChangesButtonState,
|
getDiscardExistingDocumentChangesButtonState,
|
||||||
getDiscardNewDocumentChangesButtonState,
|
getDiscardNewDocumentChangesButtonState,
|
||||||
@@ -91,7 +92,13 @@ async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | S
|
|||||||
describe("Documents tab (noSql API)", () => {
|
describe("Documents tab (noSql API)", () => {
|
||||||
describe("buildQuery", () => {
|
describe("buildQuery", () => {
|
||||||
it("should generate the right select query for SQL API", () => {
|
it("should generate the right select query for SQL API", () => {
|
||||||
expect(buildQuery(false, "")).toContain("select");
|
expect(
|
||||||
|
buildQuery(false, "", ["pk"], {
|
||||||
|
paths: ["pk"],
|
||||||
|
kind: "Hash",
|
||||||
|
version: 2,
|
||||||
|
}),
|
||||||
|
).toContain("select");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -339,7 +346,10 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
const createMockProps = (): IDocumentsTabComponentProps => ({
|
const createMockProps = (): IDocumentsTabComponentProps => ({
|
||||||
isPreferredApiMongoDB: false,
|
isPreferredApiMongoDB: false,
|
||||||
documentIds: [],
|
documentIds: [],
|
||||||
collection: undefined,
|
collection: {
|
||||||
|
id: ko.observable<string>("collectionId"),
|
||||||
|
databaseId: "databaseId",
|
||||||
|
} as ViewModels.CollectionBase,
|
||||||
partitionKey: { kind: "Hash", paths: ["/foo"], version: 2 },
|
partitionKey: { kind: "Hash", paths: ["/foo"], version: 2 },
|
||||||
onLoadStartKey: 0,
|
onLoadStartKey: 0,
|
||||||
tabTitle: "",
|
tabTitle: "",
|
||||||
@@ -380,7 +390,7 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
.findWhere((node) => node.text() === "Edit Filter")
|
.findWhere((node) => node.text() === "Edit Filter")
|
||||||
.at(0)
|
.at(0)
|
||||||
.simulate("click");
|
.simulate("click");
|
||||||
expect(wrapper.find("#filterInput").exists()).toBeTruthy();
|
expect(wrapper.find("Input.filterInput").exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -474,3 +484,13 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Documents tab", () => {
|
||||||
|
it("should add strings to array without duplicate", () => {
|
||||||
|
const array1 = ["a", "b", "c"];
|
||||||
|
const array2 = ["b", "c", "d"];
|
||||||
|
|
||||||
|
const array3 = addStringsNoDuplicate(array1, array2);
|
||||||
|
expect(array3).toEqual(["a", "b", "c", "d"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { Button, Input, TableRowId, makeStyles, shorthands } from "@fluentui/react-components";
|
import { Button, Input, TableRowId, makeStyles, shorthands } from "@fluentui/react-components";
|
||||||
import { ArrowClockwise16Filled, Dismiss16Filled } from "@fluentui/react-icons";
|
import { Dismiss16Filled } from "@fluentui/react-icons";
|
||||||
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import MongoUtility from "Common/MongoUtility";
|
import MongoUtility from "Common/MongoUtility";
|
||||||
import { StyleConstants } from "Common/StyleConstants";
|
|
||||||
import { createDocument } from "Common/dataAccess/createDocument";
|
import { createDocument } from "Common/dataAccess/createDocument";
|
||||||
import {
|
import {
|
||||||
deleteDocument as deleteNoSqlDocument,
|
deleteDocument as deleteNoSqlDocument,
|
||||||
@@ -20,6 +19,15 @@ import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
|||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
|
import {
|
||||||
|
ColumnsSelection,
|
||||||
|
FilterHistory,
|
||||||
|
SubComponentName,
|
||||||
|
TabDivider,
|
||||||
|
readSubComponentState,
|
||||||
|
saveSubComponentState,
|
||||||
|
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
||||||
|
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";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
@@ -42,13 +50,16 @@ import * as Logger from "../../../Common/Logger";
|
|||||||
import * as MongoProxyClient from "../../../Common/MongoProxyClient";
|
import * as MongoProxyClient from "../../../Common/MongoProxyClient";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
import { extractPartitionKeyValues } from "../../../Utils/QueryUtils";
|
import { defaultQueryFields, extractPartitionKeyValues } from "../../../Utils/QueryUtils";
|
||||||
import DocumentId from "../../Tree/DocumentId";
|
import DocumentId from "../../Tree/DocumentId";
|
||||||
import ObjectId from "../../Tree/ObjectId";
|
import ObjectId from "../../Tree/ObjectId";
|
||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
import { DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
import { ColumnDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
||||||
|
|
||||||
|
const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we can afford to keep more items than fit on the screen
|
||||||
|
|
||||||
const loadMoreHeight = LayoutConstants.rowHeight;
|
const loadMoreHeight = LayoutConstants.rowHeight;
|
||||||
export const useDocumentsTabStyles = makeStyles({
|
export const useDocumentsTabStyles = makeStyles({
|
||||||
@@ -92,17 +103,6 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
...shorthands.outline("1px", "dotted"),
|
...shorthands.outline("1px", "dotted"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
floatingControlsContainer: {
|
|
||||||
position: "relative",
|
|
||||||
},
|
|
||||||
floatingControls: {
|
|
||||||
position: "absolute",
|
|
||||||
top: "6px",
|
|
||||||
right: 0,
|
|
||||||
float: "right",
|
|
||||||
backgroundColor: "white",
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export class DocumentsTabV2 extends TabsBase {
|
export class DocumentsTabV2 extends TabsBase {
|
||||||
@@ -272,7 +272,7 @@ const createUploadButton = (container: Explorer): CommandButtonComponentProps =>
|
|||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
selectedCollection && container.openUploadItemsPanePane();
|
selectedCollection && container.openUploadItemsPane();
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@@ -460,17 +460,51 @@ export const showPartitionKey = (collection: ViewModels.CollectionBase, isPrefer
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Export to expose to unit tests
|
// Export to expose to unit tests
|
||||||
|
/**
|
||||||
|
* Build default query
|
||||||
|
* @param isMongo true if mongo api
|
||||||
|
* @param filter
|
||||||
|
* @param partitionKeyProperties optional for mongo
|
||||||
|
* @param partitionKey optional for mongo
|
||||||
|
* @param additionalField
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const buildQuery = (
|
export const buildQuery = (
|
||||||
isMongo: boolean,
|
isMongo: boolean,
|
||||||
filter: string,
|
filter: string,
|
||||||
partitionKeyProperties?: string[],
|
partitionKeyProperties?: string[],
|
||||||
partitionKey?: DataModels.PartitionKey,
|
partitionKey?: DataModels.PartitionKey,
|
||||||
|
additionalField?: string[],
|
||||||
): string => {
|
): string => {
|
||||||
if (isMongo) {
|
if (isMongo) {
|
||||||
return filter || "{}";
|
return filter || "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey);
|
// Filter out fields starting with "/" (partition keys)
|
||||||
|
return QueryUtils.buildDocumentsQuery(
|
||||||
|
filter,
|
||||||
|
partitionKeyProperties,
|
||||||
|
partitionKey,
|
||||||
|
additionalField?.filter((f) => !f.startsWith("/")) || [],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export to expose to unit tests
|
||||||
|
*
|
||||||
|
* Add array2 to array1 without duplicates
|
||||||
|
* @param array1
|
||||||
|
* @param array2
|
||||||
|
* @return array1 with array2 added without duplicates
|
||||||
|
*/
|
||||||
|
export const addStringsNoDuplicate = (array1: string[], array2: string[]): string[] => {
|
||||||
|
const result = [...array1];
|
||||||
|
array2.forEach((item) => {
|
||||||
|
if (!result.includes(item)) {
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export to expose to unit tests
|
// Export to expose to unit tests
|
||||||
@@ -487,6 +521,20 @@ export interface IDocumentsTabComponentProps {
|
|||||||
isTabActive: boolean;
|
isTabActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`;
|
||||||
|
|
||||||
|
const getDefaultSqlFilters = (partitionKeys: string[]) =>
|
||||||
|
['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC', "ORDER BY c._ts ASC"].concat(
|
||||||
|
partitionKeys.map((partitionKey) => `WHERE c.${partitionKey} = "foo"`),
|
||||||
|
);
|
||||||
|
const defaultMongoFilters = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"];
|
||||||
|
|
||||||
|
// Extend DocumentId to include fields displayed in the table
|
||||||
|
type ExtendedDocumentId = DocumentId & { tableFields?: DocumentsTableComponentItem };
|
||||||
|
|
||||||
|
// This is based on some heuristics
|
||||||
|
const calculateOffset = (columnNumber: number): number => columnNumber * 16 - 29;
|
||||||
|
|
||||||
// Export to expose to unit tests
|
// Export to expose to unit tests
|
||||||
export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabComponentProps> = ({
|
export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabComponentProps> = ({
|
||||||
isPreferredApiMongoDB,
|
isPreferredApiMongoDB,
|
||||||
@@ -505,7 +553,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false);
|
const [isFilterFocused, setIsFilterFocused] = useState<boolean>(false);
|
||||||
const [appliedFilter, setAppliedFilter] = useState<string>("");
|
const [appliedFilter, setAppliedFilter] = useState<string>("");
|
||||||
const [filterContent, setFilterContent] = useState<string>("");
|
const [filterContent, setFilterContent] = useState<string>("");
|
||||||
const [documentIds, setDocumentIds] = useState<DocumentId[]>([]);
|
const [documentIds, setDocumentIds] = useState<ExtendedDocumentId[]>([]);
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
const filterInput = useRef<HTMLInputElement>(null);
|
const filterInput = useRef<HTMLInputElement>(null);
|
||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
@@ -534,6 +582,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
ViewModels.DocumentExplorerState.noDocumentSelected,
|
ViewModels.DocumentExplorerState.noDocumentSelected,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// State
|
||||||
|
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
||||||
|
readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
||||||
|
leftPaneWidthPercent: 35,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const isQueryCopilotSampleContainer =
|
const isQueryCopilotSampleContainer =
|
||||||
_collection?.isSampleCollection &&
|
_collection?.isSampleCollection &&
|
||||||
_collection?.databaseId === QueryCopilotSampleDatabaseId &&
|
_collection?.databaseId === QueryCopilotSampleDatabaseId &&
|
||||||
@@ -542,6 +597,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
// For Mongo only
|
// For Mongo only
|
||||||
const [continuationToken, setContinuationToken] = useState<string>(undefined);
|
const [continuationToken, setContinuationToken] = useState<string>(undefined);
|
||||||
|
|
||||||
|
// User's filter history
|
||||||
|
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
||||||
|
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||||
|
);
|
||||||
|
|
||||||
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -567,8 +627,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
}, [documentIds, clickedRowIndex, editorState]);
|
}, [documentIds, clickedRowIndex, editorState]);
|
||||||
|
|
||||||
let lastFilterContents = ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC'];
|
|
||||||
|
|
||||||
const applyFilterButton = {
|
const applyFilterButton = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -590,10 +648,37 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
[partitionKeyPropertyHeaders],
|
[partitionKeyPropertyHeaders],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getInitialColumnSelection = () => {
|
||||||
|
const defaultColumnsIds = ["id"];
|
||||||
|
if (showPartitionKey(_collection, isPreferredApiMongoDB)) {
|
||||||
|
defaultColumnsIds.push(...partitionKeyPropertyHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultColumnsIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
||||||
|
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
||||||
|
SubComponentName.ColumnsSelection,
|
||||||
|
_collection,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!persistedColumnsSelection) {
|
||||||
|
return getInitialColumnSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedColumnsSelection.selectedColumnIds;
|
||||||
|
});
|
||||||
|
|
||||||
// new DocumentId() requires a DocumentTab which we mock with only the required properties
|
// new DocumentId() requires a DocumentTab which we mock with only the required properties
|
||||||
const newDocumentId = useCallback(
|
const newDocumentId = useCallback(
|
||||||
(rawDocument: DataModels.DocumentId, partitionKeyProperties: string[], partitionKeyValue: string[]) =>
|
(
|
||||||
new DocumentId(
|
rawDocument: DataModels.DocumentId,
|
||||||
|
partitionKeyProperties: string[],
|
||||||
|
partitionKeyValue: string[],
|
||||||
|
): ExtendedDocumentId => {
|
||||||
|
const extendedDocumentId = new DocumentId(
|
||||||
{
|
{
|
||||||
partitionKey,
|
partitionKey,
|
||||||
partitionKeyProperties,
|
partitionKeyProperties,
|
||||||
@@ -603,7 +688,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
},
|
},
|
||||||
rawDocument,
|
rawDocument,
|
||||||
partitionKeyValue,
|
partitionKeyValue,
|
||||||
),
|
) as ExtendedDocumentId;
|
||||||
|
extendedDocumentId.tableFields = { ...rawDocument };
|
||||||
|
return extendedDocumentId;
|
||||||
|
},
|
||||||
[partitionKey],
|
[partitionKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -765,6 +853,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
setDocumentIds(ids);
|
setDocumentIds(ids);
|
||||||
setEditorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
|
setEditorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
|
||||||
|
|
||||||
|
// Update column choices
|
||||||
|
setColumnDefinitionsFromDocument(savedDocument);
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.CreateDocument,
|
Action.CreateDocument,
|
||||||
{
|
{
|
||||||
@@ -847,6 +939,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
},
|
},
|
||||||
startKey,
|
startKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update column choices
|
||||||
|
selectedDocumentId.tableFields = { ...documentContent };
|
||||||
|
setColumnDefinitionsFromDocument(documentContent);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
onExecutionErrorChange(true);
|
onExecutionErrorChange(true);
|
||||||
@@ -883,7 +979,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
/**
|
/**
|
||||||
* Implementation using bulk delete NoSQL API
|
* Implementation using bulk delete NoSQL API
|
||||||
*/
|
*/
|
||||||
let _deleteDocuments = useCallback(
|
const _deleteDocuments = useCallback(
|
||||||
async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
|
async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
|
||||||
onExecutionErrorChange(false);
|
onExecutionErrorChange(false);
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocuments, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocuments, {
|
||||||
@@ -894,11 +990,29 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
|
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
|
||||||
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should always be called.
|
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should always be called.
|
||||||
return (
|
const _deleteNoSqlDocuments = async (
|
||||||
partitionKey.systemKey
|
collection: CollectionBase,
|
||||||
? deleteNoSqlDocument(_collection, toDeleteDocumentIds[0]).then(() => [toDeleteDocumentIds[0]])
|
toDeleteDocumentIds: DocumentId[],
|
||||||
: deleteNoSqlDocuments(_collection, toDeleteDocumentIds)
|
): Promise<DocumentId[]> => {
|
||||||
)
|
return partitionKey.systemKey
|
||||||
|
? deleteNoSqlDocument(collection, toDeleteDocumentIds[0]).then(() => [toDeleteDocumentIds[0]])
|
||||||
|
: deleteNoSqlDocuments(collection, toDeleteDocumentIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePromise = !isPreferredApiMongoDB
|
||||||
|
? _deleteNoSqlDocuments(_collection, toDeleteDocumentIds)
|
||||||
|
: MongoProxyClient.deleteDocuments(
|
||||||
|
_collection.databaseId,
|
||||||
|
_collection as ViewModels.Collection,
|
||||||
|
toDeleteDocumentIds,
|
||||||
|
).then(({ deletedCount, isAcknowledged }) => {
|
||||||
|
if (deletedCount === toDeleteDocumentIds.length && isAcknowledged) {
|
||||||
|
return toDeleteDocumentIds;
|
||||||
|
}
|
||||||
|
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletePromise
|
||||||
.then(
|
.then(
|
||||||
(deletedIds) => {
|
(deletedIds) => {
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
@@ -929,7 +1043,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
)
|
)
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => setIsExecuting(false));
|
||||||
},
|
},
|
||||||
[_collection, onExecutionErrorChange, tabTitle],
|
[_collection, isPreferredApiMongoDB, onExecutionErrorChange, tabTitle],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteDocuments = useCallback(
|
const deleteDocuments = useCallback(
|
||||||
@@ -954,7 +1068,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
(error: Error) =>
|
(error: Error) =>
|
||||||
useDialog
|
useDialog
|
||||||
.getState()
|
.getState()
|
||||||
.showOkModalDialog("Delete documents", `Document(s) deleted failed (${JSON.stringify(error)})`),
|
.showOkModalDialog("Delete documents", `Deleting document(s) failed (${error.message})`),
|
||||||
)
|
)
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => setIsExecuting(false));
|
||||||
},
|
},
|
||||||
@@ -1030,7 +1144,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
const _queryAbortController = new AbortController();
|
const _queryAbortController = new AbortController();
|
||||||
setQueryAbortController(_queryAbortController);
|
setQueryAbortController(_queryAbortController);
|
||||||
const filter: string = filterContent.trim();
|
const filter: string = filterContent.trim();
|
||||||
const query: string = buildQuery(isPreferredApiMongoDB, filter, partitionKeyProperties, partitionKey);
|
const query: string = buildQuery(
|
||||||
|
isPreferredApiMongoDB,
|
||||||
|
filter,
|
||||||
|
partitionKeyProperties,
|
||||||
|
partitionKey,
|
||||||
|
selectedColumnIds,
|
||||||
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const options: any = {};
|
const options: any = {};
|
||||||
// TODO: Property 'enableCrossPartitionQuery' does not exist on type 'FeedOptions'.
|
// TODO: Property 'enableCrossPartitionQuery' does not exist on type 'FeedOptions'.
|
||||||
@@ -1053,6 +1173,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
resourceTokenPartitionKey,
|
resourceTokenPartitionKey,
|
||||||
isQueryCopilotSampleContainer,
|
isQueryCopilotSampleContainer,
|
||||||
_collection,
|
_collection,
|
||||||
|
selectedColumnIds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onHideFilterClick = (): void => {
|
const onHideFilterClick = (): void => {
|
||||||
@@ -1198,16 +1319,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
documentsIterator, // loadNextPage: disabled as it will trigger a circular dependency and infinite loop
|
documentsIterator, // loadNextPage: disabled as it will trigger a circular dependency and infinite loop
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onRefreshKeyInput: KeyboardEventHandler<HTMLButtonElement> = (event) => {
|
|
||||||
if (event.key === " " || event.key === "Enter") {
|
|
||||||
const focusElement = event.target as HTMLElement;
|
|
||||||
refreshDocumentsGrid(false);
|
|
||||||
focusElement && focusElement.focus();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onLoadMoreKeyInput: KeyboardEventHandler<HTMLAnchorElement> = (event) => {
|
const onLoadMoreKeyInput: KeyboardEventHandler<HTMLAnchorElement> = (event) => {
|
||||||
if (event.key === " " || event.key === "Enter") {
|
if (event.key === " " || event.key === "Enter") {
|
||||||
const focusElement = event.target as HTMLElement;
|
const focusElement = event.target as HTMLElement;
|
||||||
@@ -1220,7 +1331,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
const onFilterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
refreshDocumentsGrid(true);
|
onApplyFilterClick();
|
||||||
|
|
||||||
// Suppress the default behavior of the key
|
// Suppress the default behavior of the key
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1239,9 +1350,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// Table config here
|
// Table config here
|
||||||
const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => {
|
const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => {
|
||||||
const item: Record<string, string> & { id: string } = {
|
const item: DocumentsTableComponentItem = documentId.tableFields || { id: documentId.id() };
|
||||||
id: documentId.id(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (partitionKeyPropertyHeaders && documentId.stringPartitionKeyValues) {
|
if (partitionKeyPropertyHeaders && documentId.stringPartitionKeyValues) {
|
||||||
for (let i = 0; i < partitionKeyPropertyHeaders.length; i++) {
|
for (let i = 0; i < partitionKeyPropertyHeaders.length; i++) {
|
||||||
@@ -1252,6 +1361,44 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const extractColumnDefinitionsFromDocument = (document: unknown): ColumnDefinition[] => {
|
||||||
|
let columnDefinitions: ColumnDefinition[] = Object.keys(document)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
.filter((key) => typeof (document as any)[key] === "string" || typeof (document as any)[key] === "number") // Only allow safe types for displayable React children
|
||||||
|
.map((key) =>
|
||||||
|
key === "id"
|
||||||
|
? { id: key, label: isPreferredApiMongoDB ? "_id" : "id", isPartitionKey: false }
|
||||||
|
: { id: key, label: key, isPartitionKey: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (showPartitionKey(_collection, isPreferredApiMongoDB)) {
|
||||||
|
columnDefinitions.push(
|
||||||
|
...partitionKeyPropertyHeaders.map((key) => ({ id: key, label: key, isPartitionKey: true })),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove properties that are the partition keys, since they are already included
|
||||||
|
columnDefinitions = columnDefinitions.filter(
|
||||||
|
(columnDefinition) => !partitionKeyProperties.includes(columnDefinition.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnDefinitions;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract column definitions from document and add to the definitions
|
||||||
|
* @param document
|
||||||
|
*/
|
||||||
|
const setColumnDefinitionsFromDocument = (document: unknown): void => {
|
||||||
|
const currentIds = new Set(columnDefinitions.map((columnDefinition) => columnDefinition.id));
|
||||||
|
extractColumnDefinitionsFromDocument(document).forEach((columnDefinition) => {
|
||||||
|
if (!currentIds.has(columnDefinition.id)) {
|
||||||
|
columnDefinitions.push(columnDefinition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setColumnDefinitions([...columnDefinitions]);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* replicate logic of selectedDocument.click();
|
* replicate logic of selectedDocument.click();
|
||||||
* Document has been clicked on in table
|
* Document has been clicked on in table
|
||||||
@@ -1267,6 +1414,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
(_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(_collection, documentId)).then(
|
(_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(_collection, documentId)).then(
|
||||||
(content) => {
|
(content) => {
|
||||||
initDocumentEditor(documentId, content);
|
initDocumentEditor(documentId, content);
|
||||||
|
|
||||||
|
// Update columns
|
||||||
|
setColumnDefinitionsFromDocument(content);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1357,10 +1507,22 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
return () => resizeObserver.disconnect(); // clean up
|
return () => resizeObserver.disconnect(); // clean up
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const columnHeaders = {
|
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
||||||
idHeader: isPreferredApiMongoDB ? "_id" : "id",
|
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
||||||
partitionKeyHeaders: (showPartitionKey(_collection, isPreferredApiMongoDB) && partitionKeyPropertyHeaders) || [],
|
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
||||||
};
|
SubComponentName.ColumnsSelection,
|
||||||
|
_collection,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!persistedColumnsSelection) {
|
||||||
|
return extractColumnDefinitionsFromDocument({
|
||||||
|
id: "id",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedColumnsSelection.columnDefinitions;
|
||||||
|
});
|
||||||
|
|
||||||
const onSelectedRowsChange = (selectedRows: Set<TableRowId>) => {
|
const onSelectedRowsChange = (selectedRows: Set<TableRowId>) => {
|
||||||
confirmDiscardingChange(() => {
|
confirmDiscardingChange(() => {
|
||||||
@@ -1423,7 +1585,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
return partitionKey;
|
return partitionKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
lastFilterContents = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"];
|
|
||||||
partitionKeyProperties = partitionKeyProperties?.map((partitionKeyProperty, i) => {
|
partitionKeyProperties = partitionKeyProperties?.map((partitionKeyProperty, i) => {
|
||||||
if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) {
|
if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) {
|
||||||
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
|
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
|
||||||
@@ -1438,62 +1599,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
return partitionKeyProperty;
|
return partitionKeyProperty;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Mongo implementation
|
|
||||||
* TODO: update proxy to use mongo driver deleteMany
|
|
||||||
*/
|
|
||||||
_deleteDocuments = (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
|
|
||||||
const promises = toDeleteDocumentIds.map((documentId) => _deleteDocument(documentId));
|
|
||||||
return Promise.all(promises);
|
|
||||||
};
|
|
||||||
|
|
||||||
const __deleteDocument = async (documentId: DocumentId): Promise<DocumentId> => {
|
|
||||||
await MongoProxyClient.deleteDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId);
|
|
||||||
return documentId;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _deleteDocument = useCallback(
|
|
||||||
(documentId: DocumentId): Promise<DocumentId> => {
|
|
||||||
onExecutionErrorChange(false);
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle,
|
|
||||||
});
|
|
||||||
setIsExecuting(true);
|
|
||||||
return __deleteDocument(documentId)
|
|
||||||
.then(
|
|
||||||
(deletedDocumentId) => {
|
|
||||||
TelemetryProcessor.traceSuccess(
|
|
||||||
Action.DeleteDocument,
|
|
||||||
{
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle,
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
return deletedDocumentId;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
onExecutionErrorChange(true);
|
|
||||||
console.error(error);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.DeleteDocument,
|
|
||||||
{
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle,
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.finally(() => setIsExecuting(false));
|
|
||||||
},
|
|
||||||
[__deleteDocument, onExecutionErrorChange, tabTitle],
|
|
||||||
);
|
|
||||||
|
|
||||||
onSaveNewDocumentClick = useCallback((): Promise<unknown> => {
|
onSaveNewDocumentClick = useCallback((): Promise<unknown> => {
|
||||||
const documentContent = JSON.parse(selectedDocumentContent);
|
const documentContent = JSON.parse(selectedDocumentContent);
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
|
||||||
@@ -1649,7 +1754,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
onExecutionErrorChange(false);
|
onExecutionErrorChange(false);
|
||||||
const filter: string = filterContent.trim();
|
const filter: string = filterContent.trim();
|
||||||
const query: string = buildQuery(isPreferredApiMongoDB, filter);
|
const query: string = buildQuery(isPreferredApiMongoDB, filter, selectedColumnIds);
|
||||||
|
|
||||||
return MongoProxyClient.queryDocuments(
|
return MongoProxyClient.queryDocuments(
|
||||||
_collection.databaseId,
|
_collection.databaseId,
|
||||||
@@ -1700,6 +1805,24 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
// ***************** Mongo ***************************
|
// ***************** Mongo ***************************
|
||||||
|
|
||||||
|
const onApplyFilterClick = (): void => {
|
||||||
|
refreshDocumentsGrid(true);
|
||||||
|
|
||||||
|
// Remove duplicates, but keep order
|
||||||
|
if (lastFilterContents.includes(filterContent)) {
|
||||||
|
lastFilterContents.splice(lastFilterContents.indexOf(filterContent), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save filter content to local storage
|
||||||
|
lastFilterContents.unshift(filterContent);
|
||||||
|
|
||||||
|
// Keep the list size under MAX_FILTER_HISTORY_COUNT. Drop last element if needed.
|
||||||
|
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
||||||
|
|
||||||
|
setLastFilterContents(limitedLastFilterContents);
|
||||||
|
saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||||
|
};
|
||||||
|
|
||||||
const refreshDocumentsGrid = useCallback(
|
const refreshDocumentsGrid = useCallback(
|
||||||
(applyFilterButtonPressed: boolean): void => {
|
(applyFilterButtonPressed: boolean): void => {
|
||||||
// clear documents grid
|
// clear documents grid
|
||||||
@@ -1730,6 +1853,41 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
[createIterator, filterContent],
|
[createIterator, filterContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onColumnSelectionChange = (newSelectedColumnIds: string[]): void => {
|
||||||
|
// Do not allow to unselecting all columns
|
||||||
|
if (newSelectedColumnIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedColumnIds(newSelectedColumnIds);
|
||||||
|
|
||||||
|
saveSubComponentState<ColumnsSelection>(SubComponentName.ColumnsSelection, _collection, {
|
||||||
|
selectedColumnIds: newSelectedColumnIds,
|
||||||
|
columnDefinitions,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevSelectedColumnIds = usePrevious({ selectedColumnIds, setSelectedColumnIds });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If we are adding a field, let's refresh to include the field in the query
|
||||||
|
let addedField = false;
|
||||||
|
for (const field of selectedColumnIds) {
|
||||||
|
if (
|
||||||
|
!defaultQueryFields.includes(field) &&
|
||||||
|
prevSelectedColumnIds &&
|
||||||
|
!prevSelectedColumnIds.selectedColumnIds.includes(field)
|
||||||
|
) {
|
||||||
|
addedField = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedField) {
|
||||||
|
refreshDocumentsGrid(false);
|
||||||
|
}
|
||||||
|
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]);
|
||||||
|
|
||||||
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" }}>
|
||||||
@@ -1758,12 +1916,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
<div className={styles.filterRow}>
|
<div className={styles.filterRow}>
|
||||||
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
||||||
<Input
|
<Input
|
||||||
id="filterInput"
|
|
||||||
ref={filterInput}
|
ref={filterInput}
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
list="filtersList"
|
list={`filtersList-${getUniqueId(_collection)}`}
|
||||||
className={styles.filterInput}
|
className={`filterInput ${styles.filterInput}`}
|
||||||
title="Type a query predicate or choose one from the list."
|
title="Type a query predicate or choose one from the list."
|
||||||
placeholder={
|
placeholder={
|
||||||
isPreferredApiMongoDB
|
isPreferredApiMongoDB
|
||||||
@@ -1777,8 +1934,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
onBlur={() => setIsFilterFocused(false)}
|
onBlur={() => setIsFilterFocused(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<datalist id="filtersList">
|
<datalist id={`filtersList-${getUniqueId(_collection)}`}>
|
||||||
{lastFilterContents.map((filter) => (
|
{addStringsNoDuplicate(
|
||||||
|
lastFilterContents,
|
||||||
|
isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
|
||||||
|
).map((filter) => (
|
||||||
<option key={filter} value={filter} />
|
<option key={filter} value={filter} />
|
||||||
))}
|
))}
|
||||||
</datalist>
|
</datalist>
|
||||||
@@ -1786,7 +1946,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => refreshDocumentsGrid(true)}
|
onClick={onApplyFilterClick}
|
||||||
disabled={!applyFilterButton.enabled}
|
disabled={!applyFilterButton.enabled}
|
||||||
aria-label="Apply filter"
|
aria-label="Apply filter"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -1817,41 +1977,45 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* <Split> doesn't like to be a flex child */}
|
{/* <Split> doesn't like to be a flex child */}
|
||||||
<div style={{ overflow: "hidden", height: "100%" }}>
|
<div style={{ overflow: "hidden", height: "100%" }}>
|
||||||
<Allotment>
|
<Allotment
|
||||||
<Allotment.Pane preferredSize="35%" minSize={175}>
|
onDragEnd={(sizes: number[]) => {
|
||||||
|
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||||
|
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||||
|
setTabStateData(tabStateData);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
||||||
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
<div style={{ height: "100%", width: "100%", overflow: "hidden" }} ref={tableContainerRef}>
|
||||||
<div className={styles.floatingControlsContainer}>
|
<div className={styles.tableContainer}>
|
||||||
<div className={styles.floatingControls}>
|
<div
|
||||||
<Button
|
style={
|
||||||
appearance="transparent"
|
{
|
||||||
aria-label="Refresh"
|
height: "100%",
|
||||||
size="small"
|
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
|
||||||
icon={<ArrowClockwise16Filled />}
|
} /* Fix to make table not resize beyond parent's width */
|
||||||
style={{
|
}
|
||||||
color: StyleConstants.AccentMedium,
|
>
|
||||||
}}
|
<DocumentsTableComponent
|
||||||
onClick={() => refreshDocumentsGrid(false)}
|
onRefreshTable={() => refreshDocumentsGrid(false)}
|
||||||
onKeyDown={onRefreshKeyInput}
|
items={tableItems}
|
||||||
|
onItemClicked={(index) => onDocumentClicked(index, documentIds)}
|
||||||
|
onSelectedRowsChange={onSelectedRowsChange}
|
||||||
|
selectedRows={selectedRows}
|
||||||
|
size={tableContainerSizePx}
|
||||||
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
columnDefinitions={columnDefinitions}
|
||||||
|
isRowSelectionDisabled={
|
||||||
|
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
||||||
|
}
|
||||||
|
onColumnSelectionChange={onColumnSelectionChange}
|
||||||
|
defaultColumnSelection={getInitialColumnSelection()}
|
||||||
|
collection={_collection}
|
||||||
|
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.tableContainer}>
|
|
||||||
<DocumentsTableComponent
|
|
||||||
items={tableItems}
|
|
||||||
onItemClicked={(index) => onDocumentClicked(index, documentIds)}
|
|
||||||
onSelectedRowsChange={onSelectedRowsChange}
|
|
||||||
selectedRows={selectedRows}
|
|
||||||
size={tableContainerSizePx}
|
|
||||||
columnHeaders={columnHeaders}
|
|
||||||
isSelectionDisabled={
|
|
||||||
(partitionKey.systemKey && !isPreferredApiMongoDB) ||
|
|
||||||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{tableItems.length > 0 && (
|
{tableItems.length > 0 && (
|
||||||
<a
|
<a
|
||||||
className={styles.loadMore}
|
className={styles.loadMore}
|
||||||
@@ -1865,7 +2029,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
<Allotment.Pane preferredSize="65%" minSize={300}>
|
<Allotment.Pane minSize={30}>
|
||||||
<div style={{ height: "100%", width: "100%" }}>
|
<div style={{ height: "100%", width: "100%" }}>
|
||||||
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
|
||||||
<EditorReact
|
<EditorReact
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { deleteDocument } from "Common/MongoProxyClient";
|
import { deleteDocuments } from "Common/MongoProxyClient";
|
||||||
import { Platform, updateConfigContext } from "ConfigContext";
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
@@ -49,7 +49,7 @@ jest.mock("Common/MongoProxyClient", () => ({
|
|||||||
id: "id1",
|
id: "id1",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
deleteDocument: jest.fn(() => Promise.resolve()),
|
deleteDocuments: jest.fn(() => Promise.resolve()),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
||||||
@@ -179,8 +179,8 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("clicking Delete Document asks for confirmation", () => {
|
it("clicking Delete Document asks for confirmation", () => {
|
||||||
const mockDeleteDocument = deleteDocument as jest.Mock;
|
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
||||||
mockDeleteDocument.mockClear();
|
mockDeleteDocuments.mockClear();
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
useCommandBar
|
useCommandBar
|
||||||
@@ -189,7 +189,7 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockDeleteDocument).toHaveBeenCalled();
|
expect(mockDeleteDocuments).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TableRowId } from "@fluentui/react-components";
|
import { TableRowId } from "@fluentui/react-components";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { DocumentsTableComponent, IDocumentsTableComponentProps } from "./DocumentsTableComponent";
|
import { DocumentsTableComponent, IDocumentsTableComponentProps } from "./DocumentsTableComponent";
|
||||||
|
|
||||||
const PARTITION_KEY_HEADER = "partitionKey";
|
const PARTITION_KEY_HEADER = "partitionKey";
|
||||||
@@ -20,11 +21,19 @@ describe("DocumentsTableComponent", () => {
|
|||||||
height: 0,
|
height: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
},
|
},
|
||||||
columnHeaders: {
|
columnDefinitions: [
|
||||||
idHeader: ID_HEADER,
|
{ id: ID_HEADER, label: "ID", isPartitionKey: false },
|
||||||
partitionKeyHeaders: [PARTITION_KEY_HEADER],
|
{ id: PARTITION_KEY_HEADER, label: "Partition Key", isPartitionKey: true },
|
||||||
|
],
|
||||||
|
isRowSelectionDisabled: false,
|
||||||
|
collection: {
|
||||||
|
databaseId: "db",
|
||||||
|
id: ((): string => "coll") as ko.Observable<string>,
|
||||||
|
} as ViewModels.CollectionBase,
|
||||||
|
onRefreshTable: (): void => {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
},
|
},
|
||||||
isSelectionDisabled: false,
|
selectedColumnIds: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render documents and partition keys in header", () => {
|
it("should render documents and partition keys in header", () => {
|
||||||
@@ -35,7 +44,7 @@ describe("DocumentsTableComponent", () => {
|
|||||||
|
|
||||||
it("should not render selection column when isSelectionDisabled is true", () => {
|
it("should not render selection column when isSelectionDisabled is true", () => {
|
||||||
const props: IDocumentsTableComponentProps = createMockProps();
|
const props: IDocumentsTableComponentProps = createMockProps();
|
||||||
props.isSelectionDisabled = true;
|
props.isRowSelectionDisabled = true;
|
||||||
const wrapper = mount(<DocumentsTableComponent {...props} />);
|
const wrapper = mount(<DocumentsTableComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,52 +1,86 @@
|
|||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Menu,
|
Menu,
|
||||||
|
MenuDivider,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
MenuPopover,
|
MenuPopover,
|
||||||
MenuTrigger,
|
MenuTrigger,
|
||||||
TableRowData as RowStateBase,
|
TableRowData as RowStateBase,
|
||||||
|
SortDirection,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableCellLayout,
|
TableCellLayout,
|
||||||
TableColumnDefinition,
|
TableColumnDefinition,
|
||||||
|
TableColumnId,
|
||||||
TableColumnSizingOptions,
|
TableColumnSizingOptions,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableHeaderCell,
|
TableHeaderCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableRowId,
|
TableRowId,
|
||||||
TableSelectionCell,
|
TableSelectionCell,
|
||||||
createTableColumn,
|
tokens,
|
||||||
useArrowNavigationGroup,
|
useArrowNavigationGroup,
|
||||||
useTableColumnSizing_unstable,
|
useTableColumnSizing_unstable,
|
||||||
useTableFeatures,
|
useTableFeatures,
|
||||||
useTableSelection,
|
useTableSelection,
|
||||||
|
useTableSort,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
|
import {
|
||||||
|
ArrowClockwise16Regular,
|
||||||
|
ArrowResetRegular,
|
||||||
|
DeleteRegular,
|
||||||
|
EditRegular,
|
||||||
|
MoreHorizontalRegular,
|
||||||
|
TableResizeColumnRegular,
|
||||||
|
TextSortAscendingRegular,
|
||||||
|
TextSortDescendingRegular,
|
||||||
|
} from "@fluentui/react-icons";
|
||||||
import { NormalizedEventKey } from "Common/Constants";
|
import { NormalizedEventKey } from "Common/Constants";
|
||||||
|
import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane";
|
||||||
|
import {
|
||||||
|
ColumnSizesMap,
|
||||||
|
ColumnSort,
|
||||||
|
deleteSubComponentState,
|
||||||
|
readSubComponentState,
|
||||||
|
saveSubComponentState,
|
||||||
|
SubComponentName,
|
||||||
|
} 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";
|
||||||
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
|
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
|
||||||
export type DocumentsTableComponentItem = {
|
export type DocumentsTableComponentItem = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Record<string, string>;
|
} & Record<string, string | number>;
|
||||||
|
|
||||||
export type ColumnHeaders = {
|
export type ColumnDefinition = {
|
||||||
idHeader: string;
|
id: string;
|
||||||
partitionKeyHeaders: string[];
|
label: string;
|
||||||
|
isPartitionKey: boolean;
|
||||||
};
|
};
|
||||||
export interface IDocumentsTableComponentProps {
|
export interface IDocumentsTableComponentProps {
|
||||||
|
onRefreshTable: () => void;
|
||||||
items: DocumentsTableComponentItem[];
|
items: DocumentsTableComponentItem[];
|
||||||
onItemClicked: (index: number) => void;
|
onItemClicked: (index: number) => void;
|
||||||
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
||||||
selectedRows: Set<TableRowId>;
|
selectedRows: Set<TableRowId>;
|
||||||
size: { height: number; width: number };
|
size: { height: number; width: number };
|
||||||
columnHeaders: ColumnHeaders;
|
selectedColumnIds: string[];
|
||||||
|
columnDefinitions: ColumnDefinition[];
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
isSelectionDisabled?: boolean;
|
isRowSelectionDisabled?: boolean;
|
||||||
|
collection: ViewModels.CollectionBase;
|
||||||
|
onColumnSelectionChange?: (newSelectedColumnIds: string[]) => void;
|
||||||
|
defaultColumnSelection?: string[];
|
||||||
|
isColumnSelectionDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
||||||
@@ -59,72 +93,203 @@ interface ReactWindowRenderFnProps extends ListChildComponentProps {
|
|||||||
data: TableRowData[];
|
data: TableRowData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const COLUMNS_MENU_NAME = "columnsMenu";
|
||||||
|
|
||||||
|
const defaultSize = {
|
||||||
|
idealWidth: 200,
|
||||||
|
minWidth: 50,
|
||||||
|
};
|
||||||
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
||||||
|
onRefreshTable,
|
||||||
items,
|
items,
|
||||||
onSelectedRowsChange,
|
onSelectedRowsChange,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
style,
|
style,
|
||||||
size,
|
size,
|
||||||
columnHeaders,
|
selectedColumnIds,
|
||||||
isSelectionDisabled,
|
columnDefinitions,
|
||||||
|
isRowSelectionDisabled: isSelectionDisabled,
|
||||||
|
collection,
|
||||||
|
onColumnSelectionChange,
|
||||||
|
defaultColumnSelection,
|
||||||
|
isColumnSelectionDisabled,
|
||||||
}: IDocumentsTableComponentProps) => {
|
}: IDocumentsTableComponentProps) => {
|
||||||
const styles = useDocumentsTabStyles();
|
const styles = useDocumentsTabStyles();
|
||||||
|
|
||||||
const initialSizingOptions: TableColumnSizingOptions = {
|
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
||||||
id: {
|
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
||||||
idealWidth: 280,
|
const columnSizesPx: TableColumnSizingOptions = {};
|
||||||
minWidth: 50,
|
selectedColumnIds.forEach((columnId) => {
|
||||||
},
|
if (
|
||||||
};
|
!columnSizesMap ||
|
||||||
columnHeaders.partitionKeyHeaders.forEach((pkHeader) => {
|
!columnSizesMap[columnId] ||
|
||||||
initialSizingOptions[pkHeader] = {
|
columnSizesMap[columnId].widthPx === undefined ||
|
||||||
idealWidth: 200,
|
isNaN(columnSizesMap[columnId].widthPx)
|
||||||
minWidth: 50,
|
) {
|
||||||
|
columnSizesPx[columnId] = defaultSize;
|
||||||
|
} else {
|
||||||
|
columnSizesPx[columnId] = {
|
||||||
|
idealWidth: columnSizesMap[columnId].widthPx,
|
||||||
|
minWidth: 50,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return columnSizesPx;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [sortState, setSortState] = React.useState<{
|
||||||
|
sortDirection: "ascending" | "descending";
|
||||||
|
sortColumn: TableColumnId | undefined;
|
||||||
|
}>(() => {
|
||||||
|
const sort = readSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, undefined);
|
||||||
|
|
||||||
|
if (!sort) {
|
||||||
|
return {
|
||||||
|
sortDirection: undefined,
|
||||||
|
sortColumn: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortDirection: sort.direction,
|
||||||
|
sortColumn: sort.columnId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(initialSizingOptions);
|
const onColumnResize = React.useCallback((_, { columnId, width }: { columnId: string; width: number }) => {
|
||||||
|
setColumnSizingOptions((state) => {
|
||||||
|
const newSizingOptions = {
|
||||||
|
...state,
|
||||||
|
[columnId]: {
|
||||||
|
...state[columnId],
|
||||||
|
idealWidth: width,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const onColumnResize = React.useCallback((_, { columnId, width }) => {
|
const persistentSizes = Object.keys(newSizingOptions).reduce((acc, key) => {
|
||||||
setColumnSizingOptions((state) => ({
|
acc[key] = {
|
||||||
...state,
|
widthPx: newSizingOptions[key].idealWidth,
|
||||||
[columnId]: {
|
};
|
||||||
...state[columnId],
|
return acc;
|
||||||
idealWidth: width,
|
}, {} as ColumnSizesMap);
|
||||||
},
|
|
||||||
}));
|
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
||||||
|
|
||||||
|
return newSizingOptions;
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// const restoreFocusTargetAttribute = useRestoreFocusTarget();
|
||||||
|
|
||||||
|
const onSortClick = (event: React.SyntheticEvent, columnId: string, direction: SortDirection) => {
|
||||||
|
setColumnSort(event, columnId, direction);
|
||||||
|
|
||||||
|
if (columnId === undefined || direction === undefined) {
|
||||||
|
deleteSubComponentState(SubComponentName.ColumnSort, collection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSubComponentState<ColumnSort>(SubComponentName.ColumnSort, collection, { 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
|
||||||
const columns: TableColumnDefinition<DocumentsTableComponentItem>[] = useMemo(
|
const columns: TableColumnDefinition<DocumentsTableComponentItem>[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[
|
columnDefinitions
|
||||||
createTableColumn<DocumentsTableComponentItem>({
|
.filter((column) => selectedColumnIds.includes(column.id))
|
||||||
columnId: "id",
|
.map((column) => ({
|
||||||
compare: (a, b) => a.id.localeCompare(b.id),
|
columnId: column.id,
|
||||||
renderHeaderCell: () => columnHeaders.idHeader,
|
compare: (a, b) => {
|
||||||
|
if (typeof a[column.id] === "string") {
|
||||||
|
return (a[column.id] as string).localeCompare(b[column.id] as string);
|
||||||
|
} else if (typeof a[column.id] === "number") {
|
||||||
|
return (a[column.id] as number) - (b[column.id] as number);
|
||||||
|
} else {
|
||||||
|
// Should not happen
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderHeaderCell: () => (
|
||||||
|
<>
|
||||||
|
<span title={column.label}>{column.label}</span>
|
||||||
|
<Menu>
|
||||||
|
<MenuTrigger disableButtonEnhancement>
|
||||||
|
<Button
|
||||||
|
// {...restoreFocusTargetAttribute}
|
||||||
|
appearance="transparent"
|
||||||
|
aria-label="Select column"
|
||||||
|
size="small"
|
||||||
|
icon={<MoreHorizontalRegular />}
|
||||||
|
style={{ position: "absolute", right: 0, backgroundColor: tokens.colorNeutralBackground1 }}
|
||||||
|
/>
|
||||||
|
</MenuTrigger>
|
||||||
|
<MenuPopover>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
||||||
|
Refresh
|
||||||
|
</MenuItem>
|
||||||
|
{userContext.features.enableDocumentsTableColumnSelection && (
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
icon={<TextSortAscendingRegular />}
|
||||||
|
onClick={(e) => onSortClick(e, column.id, "ascending")}
|
||||||
|
>
|
||||||
|
Sort ascending
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={<TextSortDescendingRegular />}
|
||||||
|
onClick={(e) => onSortClick(e, column.id, "descending")}
|
||||||
|
>
|
||||||
|
Sort descending
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon={<ArrowResetRegular />} onClick={(e) => onSortClick(e, undefined, undefined)}>
|
||||||
|
Reset sorting
|
||||||
|
</MenuItem>
|
||||||
|
{!isColumnSelectionDisabled && (
|
||||||
|
<MenuItem key="editcolumns" icon={<EditRegular />} onClick={openColumnSelectionPane}>
|
||||||
|
Edit columns
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
key="keyboardresize"
|
||||||
|
icon={<TableResizeColumnRegular />}
|
||||||
|
onClick={columnSizing.enableKeyboardMode(column.id)}
|
||||||
|
>
|
||||||
|
Resize with left/right arrow keys
|
||||||
|
</MenuItem>
|
||||||
|
{!isColumnSelectionDisabled && (
|
||||||
|
<MenuItem
|
||||||
|
key="remove"
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
// Remove column id from selectedColumnIds
|
||||||
|
const index = selectedColumnIds.indexOf(column.id);
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newSelectedColumnIds = [...selectedColumnIds];
|
||||||
|
newSelectedColumnIds.splice(index, 1);
|
||||||
|
onColumnSelectionChange(newSelectedColumnIds);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove column
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
</MenuPopover>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
),
|
||||||
renderCell: (item) => (
|
renderCell: (item) => (
|
||||||
<TableCellLayout truncate title={item.id}>
|
<TableCellLayout truncate title={`${item[column.id]}`}>
|
||||||
{item.id}
|
{item[column.id]}
|
||||||
</TableCellLayout>
|
</TableCellLayout>
|
||||||
),
|
),
|
||||||
}),
|
})),
|
||||||
].concat(
|
[columnDefinitions, onColumnSelectionChange, selectedColumnIds],
|
||||||
columnHeaders.partitionKeyHeaders.map((pkHeader) =>
|
|
||||||
createTableColumn<DocumentsTableComponentItem>({
|
|
||||||
columnId: pkHeader,
|
|
||||||
compare: (a, b) => a[pkHeader].localeCompare(b[pkHeader]),
|
|
||||||
// Show Refresh button on last column
|
|
||||||
renderHeaderCell: () => <span title={pkHeader}>{pkHeader}</span>,
|
|
||||||
renderCell: (item) => (
|
|
||||||
<TableCellLayout truncate title={item[pkHeader]}>
|
|
||||||
{item[pkHeader]}
|
|
||||||
</TableCellLayout>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
[columnHeaders],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(INITIAL_SELECTED_ROW_INDEX);
|
||||||
@@ -214,6 +379,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
columnSizing_unstable: columnSizing,
|
columnSizing_unstable: columnSizing,
|
||||||
tableRef,
|
tableRef,
|
||||||
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
|
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
|
||||||
|
sort: { getSortDirection, setColumnSort, sort },
|
||||||
} = useTableFeatures(
|
} = useTableFeatures(
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
@@ -227,25 +393,36 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
// eslint-disable-next-line react/prop-types
|
// eslint-disable-next-line react/prop-types
|
||||||
onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems),
|
onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems),
|
||||||
}),
|
}),
|
||||||
|
useTableSort({
|
||||||
|
sortState,
|
||||||
|
onSortChange: (e, nextSortState) => setSortState(nextSortState),
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const rows: TableRowData[] = getRows((row) => {
|
const headerSortProps = (columnId: TableColumnId) => ({
|
||||||
const selected = isRowSelected(row.rowId);
|
// onClick: (e: React.MouseEvent) => toggleColumnSort(e, columnId),
|
||||||
return {
|
sortDirection: getSortDirection(columnId),
|
||||||
...row,
|
|
||||||
onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId),
|
|
||||||
onKeyDown: (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
toggleRow(e, row.rowId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selected,
|
|
||||||
appearance: selected ? ("brand" as const) : ("none" as const),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rows: TableRowData[] = sort(
|
||||||
|
getRows((row) => {
|
||||||
|
const selected = isRowSelected(row.rowId);
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId),
|
||||||
|
onKeyDown: (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleRow(e, row.rowId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected,
|
||||||
|
appearance: selected ? ("brand" as const) : ("none" as const),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const toggleAllKeydown = React.useCallback(
|
const toggleAllKeydown = React.useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (e.key === " ") {
|
if (e.key === " ") {
|
||||||
@@ -271,37 +448,50 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
...style,
|
...style,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkedValues: { [COLUMNS_MENU_NAME]: string[] } = {
|
||||||
|
[COLUMNS_MENU_NAME]: [],
|
||||||
|
};
|
||||||
|
columnDefinitions.forEach(
|
||||||
|
(columnDefinition) =>
|
||||||
|
selectedColumnIds.includes(columnDefinition.id) && checkedValues[COLUMNS_MENU_NAME].push(columnDefinition.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const openColumnSelectionPane = (): void => {
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Select columns",
|
||||||
|
<TableColumnSelectionPane
|
||||||
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
columnDefinitions={columnDefinitions}
|
||||||
|
onSelectionChange={onColumnSelectionChange}
|
||||||
|
defaultSelection={defaultColumnSelection}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table noNativeElements {...tableProps}>
|
<Table noNativeElements sortable {...tableProps}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className={styles.tableRow} style={{ width: size ? size.width - 15 : "100%" }}>
|
<TableRow className={styles.tableRow} style={{ width: size ? size.width - 15 : "100%" }}>
|
||||||
{!isSelectionDisabled && (
|
{!isSelectionDisabled && (
|
||||||
<TableSelectionCell
|
<TableSelectionCell
|
||||||
|
key="selectcell"
|
||||||
checked={allRowsSelected ? true : someRowsSelected ? "mixed" : false}
|
checked={allRowsSelected ? true : someRowsSelected ? "mixed" : false}
|
||||||
onClick={toggleAllRows}
|
onClick={toggleAllRows}
|
||||||
onKeyDown={toggleAllKeydown}
|
onKeyDown={toggleAllKeydown}
|
||||||
checkboxIndicator={{ "aria-label": "Select all rows " }}
|
checkboxIndicator={{ "aria-label": "Select all rows " }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{columns.map((column /* index */) => (
|
{columns.map((column) => (
|
||||||
<Menu openOnContext key={column.columnId}>
|
<TableHeaderCell
|
||||||
<MenuTrigger>
|
className={styles.tableCell}
|
||||||
<TableHeaderCell
|
key={column.columnId}
|
||||||
className={styles.tableCell}
|
{...columnSizing.getTableHeaderCellProps(column.columnId)}
|
||||||
key={column.columnId}
|
{...headerSortProps(column.columnId)}
|
||||||
{...columnSizing.getTableHeaderCellProps(column.columnId)}
|
>
|
||||||
>
|
{column.renderHeaderCell()}
|
||||||
{column.renderHeaderCell()}
|
</TableHeaderCell>
|
||||||
</TableHeaderCell>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPopover>
|
|
||||||
<MenuList>
|
|
||||||
<MenuItem onClick={columnSizing.enableKeyboardMode(column.columnId)}>
|
|
||||||
Keyboard Column Resizing
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class to help with selection.
|
* Utility class to help with selection.
|
||||||
* This emulates File Explorer selection behavior.
|
* This emulates File Explorer selection behavior.
|
||||||
@@ -90,3 +92,12 @@ export const selectionHelper = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// To get previous values of a state in useEffect
|
||||||
|
export const usePrevious = <T>(value: T): T | undefined => {
|
||||||
|
const ref = useRef<T>();
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
});
|
||||||
|
return ref.current;
|
||||||
|
};
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Allotment>
|
<Allotment
|
||||||
|
onDragEnd={[Function]}
|
||||||
|
>
|
||||||
<Allotment.Pane
|
<Allotment.Pane
|
||||||
minSize={175}
|
minSize={55}
|
||||||
preferredSize="35%"
|
preferredSize="35%"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -53,52 +55,61 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___77lcry0_0000000 f10pi13n"
|
className="___9o87uj0_0000000 ffefeo0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___1rwkz4r_0000000 f1euv43f f1l8gmrm f1e31b4d f150nix6 fy6ml6n f19g0ac"
|
style={
|
||||||
|
{
|
||||||
|
"height": "100%",
|
||||||
|
"width": "calc(100% + -13px)",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<DocumentsTableComponent
|
||||||
appearance="transparent"
|
collection={
|
||||||
aria-label="Refresh"
|
|
||||||
icon={<ArrowClockwise16Filled />}
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
size="small"
|
|
||||||
style={
|
|
||||||
{
|
{
|
||||||
"color": undefined,
|
"databaseId": "databaseId",
|
||||||
|
"id": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
columnDefinitions={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "id",
|
||||||
|
"isPartitionKey": false,
|
||||||
|
"label": "id",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
defaultColumnSelection={
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
isColumnSelectionDisabled={false}
|
||||||
|
isRowSelectionDisabled={true}
|
||||||
|
items={[]}
|
||||||
|
onColumnSelectionChange={[Function]}
|
||||||
|
onItemClicked={[Function]}
|
||||||
|
onRefreshTable={[Function]}
|
||||||
|
onSelectedRowsChange={[Function]}
|
||||||
|
selectedColumnIds={
|
||||||
|
[
|
||||||
|
"id",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
selectedRows={
|
||||||
|
Set {
|
||||||
|
0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="___9o87uj0_0000000 ffefeo0"
|
|
||||||
>
|
|
||||||
<DocumentsTableComponent
|
|
||||||
columnHeaders={
|
|
||||||
{
|
|
||||||
"idHeader": "id",
|
|
||||||
"partitionKeyHeaders": [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isSelectionDisabled={true}
|
|
||||||
items={[]}
|
|
||||||
onItemClicked={[Function]}
|
|
||||||
onSelectedRowsChange={[Function]}
|
|
||||||
selectedRows={
|
|
||||||
Set {
|
|
||||||
0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
<Allotment.Pane
|
<Allotment.Pane
|
||||||
minSize={300}
|
minSize={30}
|
||||||
preferredSize="65%"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import { IMessageBarStyles, Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
import { IMessageBarStyles, Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext, updateConfigContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { IpRule } from "Contracts/DataModels";
|
import { IpRule } from "Contracts/DataModels";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
@@ -100,8 +100,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{`To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove
|
{`Data Explorer has a 5,000 RU default limit. To adjust the limit, go to the Settings page and find "RU Threshold".`}
|
||||||
the limit, go to the Settings cog on the right and find "RU Threshold".`}
|
|
||||||
<Link
|
<Link
|
||||||
className="underlinedLink"
|
className="underlinedLink"
|
||||||
href="https://review.learn.microsoft.com/en-us/azure/cosmos-db/data-explorer?branch=main#configure-request-unit-threshold"
|
href="https://review.learn.microsoft.com/en-us/azure/cosmos-db/data-explorer?branch=main#configure-request-unit-threshold"
|
||||||
@@ -119,7 +118,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{`We are moving our middleware to new infrastructure. To avoid future issues with Data Explorer access, please
|
{`We have migrated our middleware to new infrastructure. To avoid issues with Data Explorer access, please
|
||||||
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
@@ -398,12 +397,6 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
|||||||
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ipRulesIncludeMongoProxy) {
|
|
||||||
updateConfigContext({
|
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return !ipRulesIncludeMongoProxy;
|
return !ipRulesIncludeMongoProxy;
|
||||||
} else if (userContext.apiType === "Cassandra") {
|
} else if (userContext.apiType === "Cassandra") {
|
||||||
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
||||||
@@ -422,12 +415,6 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
|||||||
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ipRulesIncludeCassandraProxy) {
|
|
||||||
updateConfigContext({
|
|
||||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return !ipRulesIncludeCassandraProxy;
|
return !ipRulesIncludeCassandraProxy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import React from "react";
|
|||||||
import { appThemeFabricTealBrandRamp } from "../../Platform/Fabric/FabricTheme";
|
import { appThemeFabricTealBrandRamp } from "../../Platform/Fabric/FabricTheme";
|
||||||
|
|
||||||
export const LayoutConstants = {
|
export const LayoutConstants = {
|
||||||
rowHeight: 36,
|
rowHeight: 32,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Our CosmosFluentProvider has the same props as a FluentProvider.
|
// Our CosmosFluentProvider has the same props as a FluentProvider.
|
||||||
@@ -91,15 +91,30 @@ const appThemePortalBrandRamp: BrandVariants = {
|
|||||||
160: "#CDD8EF",
|
160: "#CDD8EF",
|
||||||
};
|
};
|
||||||
|
|
||||||
const cosmosThemeElements = {
|
export enum LayoutSize {
|
||||||
layoutRowHeight: `${LayoutConstants.rowHeight}px`,
|
Compact,
|
||||||
|
// TODO: Cozy and Roomy layouts.
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CosmosThemeElements {
|
||||||
|
layoutRowHeight: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CosmosTheme = Theme & CosmosThemeElements;
|
||||||
|
|
||||||
|
const sizeMappings: Record<LayoutSize, Partial<Theme> & CosmosThemeElements> = {
|
||||||
|
[LayoutSize.Compact]: {
|
||||||
|
layoutRowHeight: "32px",
|
||||||
|
fontSizeBase300: "13px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const cosmosTheme = {
|
||||||
sidebarMinimumWidth: "200px",
|
sidebarMinimumWidth: "200px",
|
||||||
sidebarInitialWidth: "300px",
|
sidebarInitialWidth: "300px",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CosmosTheme = Theme & typeof cosmosThemeElements;
|
export const tokens = themeToTokensObject({ ...webLightTheme, ...cosmosTheme, ...sizeMappings[LayoutSize.Compact] });
|
||||||
|
|
||||||
export const tokens = themeToTokensObject({ ...webLightTheme, ...cosmosThemeElements });
|
|
||||||
|
|
||||||
export const cosmosShorthands = {
|
export const cosmosShorthands = {
|
||||||
border: () => shorthands.border("1px", "solid", tokens.colorNeutralStroke2),
|
border: () => shorthands.border("1px", "solid", tokens.colorNeutralStroke2),
|
||||||
@@ -117,6 +132,7 @@ export function getPlatformTheme(platform: Platform): CosmosTheme {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...baseTheme,
|
...baseTheme,
|
||||||
...cosmosThemeElements,
|
...cosmosTheme,
|
||||||
|
...sizeMappings[LayoutSize.Compact], // TODO: Allow for different layout sizes.
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardCollection",
|
"label": "standardCollection",
|
||||||
@@ -69,6 +72,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "conflictsCollection",
|
"label": "conflictsCollection",
|
||||||
@@ -92,6 +98,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardDb",
|
"label": "standardDb",
|
||||||
@@ -102,6 +111,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
|
"iconSrc": <SettingsRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"id": "",
|
"id": "",
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "Scale",
|
"label": "Scale",
|
||||||
@@ -133,6 +145,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sampleItemsCollection",
|
"label": "sampleItemsCollection",
|
||||||
@@ -156,6 +171,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sharedDatabase",
|
"label": "sharedDatabase",
|
||||||
@@ -246,6 +264,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "schemaCollection",
|
"label": "schemaCollection",
|
||||||
@@ -274,6 +295,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "giganticDatabase",
|
"label": "giganticDatabase",
|
||||||
@@ -345,6 +369,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardCollection",
|
"label": "standardCollection",
|
||||||
@@ -415,6 +442,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "conflictsCollection",
|
"label": "conflictsCollection",
|
||||||
@@ -438,6 +468,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardDb",
|
"label": "standardDb",
|
||||||
@@ -448,6 +481,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
|
"iconSrc": <SettingsRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"id": "",
|
"id": "",
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "Scale",
|
"label": "Scale",
|
||||||
@@ -510,6 +546,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sampleItemsCollection",
|
"label": "sampleItemsCollection",
|
||||||
@@ -533,6 +572,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sharedDatabase",
|
"label": "sharedDatabase",
|
||||||
@@ -654,6 +696,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "schemaCollection",
|
"label": "schemaCollection",
|
||||||
@@ -682,6 +727,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "giganticDatabase",
|
"label": "giganticDatabase",
|
||||||
@@ -706,6 +754,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardCollection",
|
"label": "standardCollection",
|
||||||
@@ -724,6 +775,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "conflictsCollection",
|
"label": "conflictsCollection",
|
||||||
@@ -747,6 +801,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardDb",
|
"label": "standardDb",
|
||||||
@@ -766,6 +823,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sampleItemsCollection",
|
"label": "sampleItemsCollection",
|
||||||
@@ -789,6 +849,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sharedDatabase",
|
"label": "sharedDatabase",
|
||||||
@@ -808,6 +871,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "schemaCollection",
|
"label": "schemaCollection",
|
||||||
@@ -836,6 +902,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "giganticDatabase",
|
"label": "giganticDatabase",
|
||||||
@@ -976,6 +1045,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardCollection",
|
"label": "standardCollection",
|
||||||
@@ -1076,6 +1148,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "conflictsCollection",
|
"label": "conflictsCollection",
|
||||||
@@ -1099,6 +1174,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardDb",
|
"label": "standardDb",
|
||||||
@@ -1109,6 +1187,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
|
"iconSrc": <SettingsRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"id": "",
|
"id": "",
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "Scale",
|
"label": "Scale",
|
||||||
@@ -1201,6 +1282,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sampleItemsCollection",
|
"label": "sampleItemsCollection",
|
||||||
@@ -1224,6 +1308,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sharedDatabase",
|
"label": "sharedDatabase",
|
||||||
@@ -1375,6 +1462,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "schemaCollection",
|
"label": "schemaCollection",
|
||||||
@@ -1403,6 +1493,9 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "giganticDatabase",
|
"label": "giganticDatabase",
|
||||||
@@ -1543,6 +1636,9 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardCollection",
|
"label": "standardCollection",
|
||||||
@@ -1638,6 +1734,9 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "conflictsCollection",
|
"label": "conflictsCollection",
|
||||||
@@ -1661,6 +1760,9 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "standardDb",
|
"label": "standardDb",
|
||||||
@@ -1671,6 +1773,9 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
|
"iconSrc": <SettingsRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"id": "",
|
"id": "",
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "Scale",
|
"label": "Scale",
|
||||||
@@ -1763,6 +1868,9 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sampleItemsCollection",
|
"label": "sampleItemsCollection",
|
||||||
@@ -1786,6 +1894,9 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "sharedDatabase",
|
"label": "sharedDatabase",
|
||||||
@@ -1937,6 +2048,9 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "schemaCollection",
|
"label": "schemaCollection",
|
||||||
@@ -1965,6 +2079,9 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteDatabaseMenuItem",
|
"styleClass": "deleteDatabaseMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DatabaseRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "giganticDatabase",
|
"label": "giganticDatabase",
|
||||||
@@ -1986,6 +2103,9 @@ exports[`createResourceTokenTreeNodes creates the expected tree nodes 1`] = `
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "testCollection",
|
"label": "testCollection",
|
||||||
@@ -2021,6 +2141,9 @@ exports[`createSampleDataTreeNodes creates the expected tree nodes 1`] = `
|
|||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"iconSrc": <DocumentMultipleRegular
|
||||||
|
fontSize={16}
|
||||||
|
/>,
|
||||||
"isExpanded": false,
|
"isExpanded": false,
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "testCollection",
|
"label": "testCollection",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons";
|
||||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
||||||
@@ -7,6 +8,7 @@ import { useDatabases } from "Explorer/useDatabases";
|
|||||||
import { getItemName } from "Utils/APITypeUtils";
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
import React from "react";
|
||||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||||
import { Platform, configContext } from "../../ConfigContext";
|
import { Platform, configContext } from "../../ConfigContext";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
@@ -25,6 +27,10 @@ export const shouldShowScriptNodes = (): boolean => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
||||||
|
const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
|
||||||
|
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
|
||||||
|
|
||||||
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
|
||||||
const updatedSampleTree: TreeNode = {
|
const updatedSampleTree: TreeNode = {
|
||||||
label: sampleDataResourceTokenCollection.databaseId,
|
label: sampleDataResourceTokenCollection.databaseId,
|
||||||
@@ -36,6 +42,7 @@ export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: Vie
|
|||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
||||||
|
iconSrc: TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
|
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
|
||||||
useCommandBar.getState().setContextButtons([]);
|
useCommandBar.getState().setContextButtons([]);
|
||||||
@@ -104,6 +111,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
|
|||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
children,
|
children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
|
iconSrc: TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// Rewritten version of expandCollapseCollection
|
// Rewritten version of expandCollapseCollection
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
@@ -133,6 +141,7 @@ export const createDatabaseTreeNodes = (
|
|||||||
databaseNode.children.push({
|
databaseNode.children.push({
|
||||||
id: database.isSampleDB ? "sampleScaleSettings" : "",
|
id: database.isSampleDB ? "sampleScaleSettings" : "",
|
||||||
label: "Scale",
|
label: "Scale",
|
||||||
|
iconSrc: TreeSettingsIcon,
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
.getState()
|
.getState()
|
||||||
@@ -169,6 +178,7 @@ export const createDatabaseTreeNodes = (
|
|||||||
children: [],
|
children: [],
|
||||||
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
||||||
|
iconSrc: TreeDatabaseIcon,
|
||||||
onExpanded: async () => {
|
onExpanded: async () => {
|
||||||
useSelectedNode.getState().setSelectedNode(database);
|
useSelectedNode.getState().setSelectedNode(database);
|
||||||
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
||||||
@@ -219,6 +229,7 @@ export const buildCollectionNode = (
|
|||||||
children: children,
|
children: children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
||||||
|
iconSrc: TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
@@ -52,7 +52,7 @@ export const isAccountRestrictedForConnectionStringLogin = async (connectionStri
|
|||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append(HttpHeaders.connectionString, connectionString);
|
headers.append(HttpHeaders.connectionString, connectionString);
|
||||||
|
|
||||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.AccountRestrictions)
|
||||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||||
: configContext.BACKEND_ENDPOINT;
|
: configContext.BACKEND_ENDPOINT;
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type Features = {
|
|||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
|
readonly enableDocumentsTableColumnSelection: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -108,6 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
|
enableDocumentsTableColumnSelection: "true" === get("enabledocumentstablecolumnselection"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
170
src/Shared/AppStatePersistenceUtility.test.ts
Normal file
170
src/Shared/AppStatePersistenceUtility.test.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { createKeyFromPath, deleteState, loadState, MAX_ENTRY_NB, saveState } from "Shared/AppStatePersistenceUtility";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
|
||||||
|
jest.mock("Shared/StorageUtility", () => ({
|
||||||
|
LocalStorageUtility: {
|
||||||
|
getEntryObject: jest.fn(),
|
||||||
|
setEntryObject: jest.fn(),
|
||||||
|
},
|
||||||
|
StorageKey: {
|
||||||
|
AppState: "AppState",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("AppStatePersistenceUtility", () => {
|
||||||
|
const storePath = {
|
||||||
|
componentName: "a",
|
||||||
|
subComponentName: "b",
|
||||||
|
globalAccountName: "c",
|
||||||
|
databaseName: "d",
|
||||||
|
containerName: "e",
|
||||||
|
};
|
||||||
|
const key = createKeyFromPath(storePath);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({
|
||||||
|
key0: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
timestamp: 0,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("saveState()", () => {
|
||||||
|
const testState = { aa: 1, bb: "2", cc: [3, 4] };
|
||||||
|
|
||||||
|
it("should save state", () => {
|
||||||
|
saveState(storePath, testState);
|
||||||
|
expect(LocalStorageUtility.setEntryObject).toHaveBeenCalledTimes(1);
|
||||||
|
expect(LocalStorageUtility.setEntryObject).toHaveBeenCalledWith(StorageKey.AppState, expect.any(Object));
|
||||||
|
|
||||||
|
const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
||||||
|
expect(passedState[key].data).toHaveProperty("aa", 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should save state with timestamp", () => {
|
||||||
|
saveState(storePath, testState);
|
||||||
|
const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
||||||
|
expect(passedState[key]).toHaveProperty("timestamp");
|
||||||
|
expect(passedState[key].timestamp).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add state to existing state", () => {
|
||||||
|
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({
|
||||||
|
key0: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
timestamp: 0,
|
||||||
|
data: { dd: 5 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
saveState(storePath, testState);
|
||||||
|
const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
||||||
|
expect(passedState["key0"].data).toHaveProperty("dd", 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove the oldest entry when the number of entries exceeds the limit", () => {
|
||||||
|
// Fill up storage with MAX entries
|
||||||
|
const currentAppState = {};
|
||||||
|
for (let i = 0; i < MAX_ENTRY_NB; i++) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(currentAppState as any)[`key${i}`] = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
timestamp: i,
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue(currentAppState);
|
||||||
|
|
||||||
|
saveState(storePath, testState);
|
||||||
|
|
||||||
|
// Verify that the new entry is saved
|
||||||
|
const passedState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
||||||
|
expect(passedState[key].data).toHaveProperty("aa", 1);
|
||||||
|
|
||||||
|
// Verify that the oldest entry is removed (smallest timestamp)
|
||||||
|
const passedAppState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
||||||
|
expect(Object.keys(passedAppState).length).toBe(MAX_ENTRY_NB);
|
||||||
|
expect(passedAppState).not.toHaveProperty("key0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not remove the oldest entry when the number of entries does not exceed the limit", () => {
|
||||||
|
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({
|
||||||
|
key0: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
timestamp: 0,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
key1: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
timestamp: 1,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
saveState(storePath, testState);
|
||||||
|
const passedAppState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
||||||
|
expect(Object.keys(passedAppState).length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("loadState()", () => {
|
||||||
|
it("should load state", () => {
|
||||||
|
const data = { aa: 1, bb: "2", cc: [3, 4] };
|
||||||
|
const testState = {
|
||||||
|
[key]: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
timestamp: 0,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue(testState);
|
||||||
|
const state = loadState(storePath);
|
||||||
|
expect(state).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined if the state is not found", () => {
|
||||||
|
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue(null);
|
||||||
|
const state = loadState(storePath);
|
||||||
|
expect(state).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteState()", () => {
|
||||||
|
it("should delete state", () => {
|
||||||
|
const key = createKeyFromPath(storePath);
|
||||||
|
(LocalStorageUtility.getEntryObject as jest.Mock).mockReturnValue({
|
||||||
|
[key]: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
timestamp: 0,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
otherKey: {
|
||||||
|
schemaVersion: 2,
|
||||||
|
timestamp: 0,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteState(storePath);
|
||||||
|
expect(LocalStorageUtility.setEntryObject).toHaveBeenCalledTimes(1);
|
||||||
|
const passedAppState = (LocalStorageUtility.setEntryObject as jest.Mock).mock.calls[0][1];
|
||||||
|
expect(passedAppState).not.toHaveProperty(key);
|
||||||
|
expect(passedAppState).toHaveProperty("otherKey");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("createKeyFromPath()", () => {
|
||||||
|
it("should create path that contains all components", () => {
|
||||||
|
const key = createKeyFromPath(storePath);
|
||||||
|
expect(key).toContain(storePath.componentName);
|
||||||
|
expect(key).toContain(storePath.subComponentName);
|
||||||
|
expect(key).toContain(storePath.globalAccountName);
|
||||||
|
expect(key).toContain(storePath.databaseName);
|
||||||
|
expect(key).toContain(storePath.containerName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
109
src/Shared/AppStatePersistenceUtility.ts
Normal file
109
src/Shared/AppStatePersistenceUtility.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
|
||||||
|
// The component name whose state is being saved. Component name must not include special characters.
|
||||||
|
export type ComponentName = "DocumentsTab";
|
||||||
|
|
||||||
|
const SCHEMA_VERSION = 1;
|
||||||
|
|
||||||
|
// Export for testing purposes
|
||||||
|
export const MAX_ENTRY_NB = 100_000; // Limit number of entries to 100k
|
||||||
|
|
||||||
|
export interface StateData {
|
||||||
|
schemaVersion: number;
|
||||||
|
timestamp: number;
|
||||||
|
data: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorePath = {
|
||||||
|
componentName: string;
|
||||||
|
subComponentName?: string;
|
||||||
|
globalAccountName?: string;
|
||||||
|
databaseName?: string;
|
||||||
|
containerName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load and save state data
|
||||||
|
export const loadState = (path: StorePath): unknown => {
|
||||||
|
const appState =
|
||||||
|
LocalStorageUtility.getEntryObject<ApplicationState>(StorageKey.AppState) || ({} as ApplicationState);
|
||||||
|
const key = createKeyFromPath(path);
|
||||||
|
return appState[key]?.data;
|
||||||
|
};
|
||||||
|
export const saveState = (path: StorePath, state: unknown): void => {
|
||||||
|
// Retrieve state object
|
||||||
|
const appState =
|
||||||
|
LocalStorageUtility.getEntryObject<ApplicationState>(StorageKey.AppState) || ({} as ApplicationState);
|
||||||
|
const key = createKeyFromPath(path);
|
||||||
|
appState[key] = {
|
||||||
|
schemaVersion: SCHEMA_VERSION,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
data: state,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(appState).length > MAX_ENTRY_NB) {
|
||||||
|
// Remove the oldest entry
|
||||||
|
const oldestKey = Object.keys(appState).reduce((oldest, current) =>
|
||||||
|
appState[current].timestamp < appState[oldest].timestamp ? current : oldest,
|
||||||
|
);
|
||||||
|
delete appState[oldestKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalStorageUtility.setEntryObject(StorageKey.AppState, appState);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteState = (path: StorePath): void => {
|
||||||
|
// Retrieve state object
|
||||||
|
const appState =
|
||||||
|
LocalStorageUtility.getEntryObject<ApplicationState>(StorageKey.AppState) || ({} as ApplicationState);
|
||||||
|
const key = createKeyFromPath(path);
|
||||||
|
delete appState[key];
|
||||||
|
LocalStorageUtility.setEntryObject(StorageKey.AppState, appState);
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is for high-frequency state changes
|
||||||
|
let timeoutId: NodeJS.Timeout | undefined;
|
||||||
|
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ApplicationState {
|
||||||
|
[statePath: string]: StateData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderedPathSegments: (keyof StorePath)[] = [
|
||||||
|
"subComponentName",
|
||||||
|
"globalAccountName",
|
||||||
|
"databaseName",
|
||||||
|
"containerName",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /componentName/subComponentName/globalAccountName/databaseName/containerName/
|
||||||
|
* Any of the path segments can be "" except componentName
|
||||||
|
* Export for testing purposes
|
||||||
|
* @param path
|
||||||
|
*/
|
||||||
|
export const createKeyFromPath = (path: StorePath): string => {
|
||||||
|
if (path.componentName.includes("/")) {
|
||||||
|
throw new Error(`Invalid component name: ${path.componentName}`);
|
||||||
|
}
|
||||||
|
let key = `/${path.componentName}`; // ComponentName is always there
|
||||||
|
orderedPathSegments.forEach((segment) => {
|
||||||
|
const segmentValue = path[segment as keyof StorePath];
|
||||||
|
if (segmentValue.includes("/")) {
|
||||||
|
throw new Error(`Invalid setting path segment: ${segment}`);
|
||||||
|
}
|
||||||
|
key += `/${segmentValue !== undefined ? segmentValue : ""}`;
|
||||||
|
});
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the entire app state key from local storage
|
||||||
|
*/
|
||||||
|
export const deleteAllStates = (): void => {
|
||||||
|
LocalStorageUtility.removeEntry(StorageKey.AppState);
|
||||||
|
};
|
||||||
@@ -20,3 +20,14 @@ export const setEntryNumber = (key: StorageKey, value: number): void =>
|
|||||||
|
|
||||||
export const setEntryBoolean = (key: StorageKey, value: boolean): void =>
|
export const setEntryBoolean = (key: StorageKey, value: boolean): void =>
|
||||||
localStorage.setItem(StorageKey[key], value.toString());
|
localStorage.setItem(StorageKey[key], value.toString());
|
||||||
|
|
||||||
|
export const setEntryObject = (key: StorageKey, value: unknown): void => {
|
||||||
|
localStorage.setItem(StorageKey[key], JSON.stringify(value));
|
||||||
|
};
|
||||||
|
export const getEntryObject = <T>(key: StorageKey): T | null => {
|
||||||
|
const item = localStorage.getItem(StorageKey[key]);
|
||||||
|
if (item) {
|
||||||
|
return JSON.parse(item) as T;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export enum StorageKey {
|
|||||||
GalleryCalloutDismissed,
|
GalleryCalloutDismissed,
|
||||||
VisitedAccounts,
|
VisitedAccounts,
|
||||||
PriorityLevel,
|
PriorityLevel,
|
||||||
|
DocumentsTabPrefs,
|
||||||
DefaultQueryResultsView,
|
DefaultQueryResultsView,
|
||||||
|
AppState,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasRUThresholdBeenConfigured = (): boolean => {
|
export const hasRUThresholdBeenConfigured = (): boolean => {
|
||||||
@@ -56,10 +58,10 @@ export const getRUThreshold = (): number => {
|
|||||||
|
|
||||||
export const getDefaultQueryResultsView = (): SplitterDirection => {
|
export const getDefaultQueryResultsView = (): SplitterDirection => {
|
||||||
const defaultQueryResultsViewRaw = LocalStorageUtility.getEntryString(StorageKey.DefaultQueryResultsView);
|
const defaultQueryResultsViewRaw = LocalStorageUtility.getEntryString(StorageKey.DefaultQueryResultsView);
|
||||||
if (defaultQueryResultsViewRaw === SplitterDirection.Horizontal) {
|
if (defaultQueryResultsViewRaw === SplitterDirection.Vertical) {
|
||||||
return SplitterDirection.Horizontal;
|
return SplitterDirection.Vertical;
|
||||||
}
|
}
|
||||||
return SplitterDirection.Vertical;
|
return SplitterDirection.Horizontal;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DefaultRUThreshold = 5000;
|
export const DefaultRUThreshold = 5000;
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ export enum Action {
|
|||||||
QueryEdited,
|
QueryEdited,
|
||||||
ExecuteQueryGeneratedFromQueryCopilot,
|
ExecuteQueryGeneratedFromQueryCopilot,
|
||||||
DeleteDocuments,
|
DeleteDocuments,
|
||||||
|
ReadPersistedTabState,
|
||||||
|
SavePersistedTabState,
|
||||||
|
DeletePersistedTabState,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionModifiers = {
|
export const ActionModifiers = {
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ export const defaultAllowedArmEndpoints: ReadonlyArray<string> = [
|
|||||||
"https://management.chinacloudapi.cn",
|
"https://management.chinacloudapi.cn",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const allowedAadEndpoints: ReadonlyArray<string> = ["https://login.microsoftonline.com/"];
|
export const allowedAadEndpoints: ReadonlyArray<string> = [
|
||||||
|
"https://login.microsoftonline.com/",
|
||||||
|
"https://login.microsoftonline.us/",
|
||||||
|
"https://login.partner.microsoftonline.cn/",
|
||||||
|
];
|
||||||
|
|
||||||
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
||||||
"https://main.documentdb.ext.azure.com",
|
"https://main.documentdb.ext.azure.com",
|
||||||
@@ -74,6 +78,13 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
|
|||||||
//usnat: ["7.28.202.68"],
|
//usnat: ["7.28.202.68"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PortalBackendOutboundIPs: { [key: string]: string[] } = {
|
||||||
|
[PortalBackendEndpoints.Mpac]: ["13.91.105.215", "4.210.172.107"],
|
||||||
|
[PortalBackendEndpoints.Prod]: ["13.88.56.148", "40.91.218.243"],
|
||||||
|
[PortalBackendEndpoints.Fairfax]: ["52.247.163.6", "52.244.134.181"],
|
||||||
|
[PortalBackendEndpoints.Mooncake]: ["163.228.137.6", "143.64.170.142"],
|
||||||
|
};
|
||||||
|
|
||||||
export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
||||||
[MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"],
|
[MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"],
|
||||||
[MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"],
|
[MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"],
|
||||||
@@ -164,7 +175,23 @@ export function useNewPortalBackendEndpoint(backendApi: string): boolean {
|
|||||||
PortalBackendEndpoints.Mpac,
|
PortalBackendEndpoints.Mpac,
|
||||||
PortalBackendEndpoints.Prod,
|
PortalBackendEndpoints.Prod,
|
||||||
],
|
],
|
||||||
[BackendApi.AccountRestrictions]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
|
[BackendApi.AccountRestrictions]: [
|
||||||
|
PortalBackendEndpoints.Development,
|
||||||
|
PortalBackendEndpoints.Mpac,
|
||||||
|
PortalBackendEndpoints.Prod,
|
||||||
|
],
|
||||||
|
[BackendApi.RuntimeProxy]: [
|
||||||
|
PortalBackendEndpoints.Development,
|
||||||
|
PortalBackendEndpoints.Mpac,
|
||||||
|
PortalBackendEndpoints.Prod,
|
||||||
|
],
|
||||||
|
[BackendApi.DisallowedLocations]: [
|
||||||
|
PortalBackendEndpoints.Development,
|
||||||
|
PortalBackendEndpoints.Mpac,
|
||||||
|
PortalBackendEndpoints.Prod,
|
||||||
|
PortalBackendEndpoints.Fairfax,
|
||||||
|
PortalBackendEndpoints.Mooncake,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
|
import { MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||||
import { resetConfigContext, updateConfigContext } from "ConfigContext";
|
import { resetConfigContext, updateConfigContext } from "ConfigContext";
|
||||||
import { DatabaseAccount, IpRule } from "Contracts/DataModels";
|
import { DatabaseAccount, IpRule } from "Contracts/DataModels";
|
||||||
import { updateUserContext } from "UserContext";
|
import { updateUserContext } from "UserContext";
|
||||||
import { PortalBackendIPs } from "Utils/EndpointUtils";
|
import { MongoProxyOutboundIPs, PortalBackendIPs, PortalBackendOutboundIPs } from "Utils/EndpointUtils";
|
||||||
import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
|
||||||
|
|
||||||
describe("NetworkUtility tests", () => {
|
describe("NetworkUtility tests", () => {
|
||||||
describe("getNetworkSettingsWarningMessage", () => {
|
describe("getNetworkSettingsWarningMessage", () => {
|
||||||
|
const legacyBackendEndpoint: string = "https://main.documentdb.ext.azure.com";
|
||||||
const publicAccessMessagePart = "Please enable public access to proceed";
|
const publicAccessMessagePart = "Please enable public access to proceed";
|
||||||
const accessMessagePart = "Please allow access from Azure Portal to proceed";
|
const accessMessagePart = "Please allow access from Azure Portal to proceed";
|
||||||
// validEnpoints are a subset of those from Utils/EndpointValidation/PortalBackendIPs
|
|
||||||
const validEndpoints = [
|
|
||||||
"https://main.documentdb.ext.azure.com",
|
|
||||||
"https://main.documentdb.ext.azure.cn",
|
|
||||||
"https://main.documentdb.ext.azure.us",
|
|
||||||
];
|
|
||||||
|
|
||||||
let warningMessageResult: string;
|
let warningMessageResult: string;
|
||||||
const warningMessageFunc = (msg: string) => (warningMessageResult = msg);
|
const warningMessageFunc = (msg: string) => (warningMessageResult = msg);
|
||||||
|
|
||||||
@@ -52,52 +47,59 @@ describe("NetworkUtility tests", () => {
|
|||||||
expect(warningMessageResult).toContain(publicAccessMessagePart);
|
expect(warningMessageResult).toContain(publicAccessMessagePart);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should return no message when the appropriate ip rules are added to mongo/cassandra account per endpoint`, () => {
|
it(`should return no message when the appropriate ip rules are added to mongo/cassandra account per endpoint`, async () => {
|
||||||
validEndpoints.forEach(async (endpoint) => {
|
const portalBackendOutboundIPsWithLegacyIPs: string[] = [
|
||||||
updateUserContext({
|
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
|
||||||
databaseAccount: {
|
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
|
||||||
kind: "MongoDB",
|
...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac],
|
||||||
properties: {
|
...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod],
|
||||||
ipRules: PortalBackendIPs[endpoint].map((ip: string) => ({ ipAddressOrRange: ip }) as IpRule),
|
...PortalBackendIPs["https://main.documentdb.ext.azure.com"],
|
||||||
publicNetworkAccess: "Enabled",
|
];
|
||||||
},
|
updateUserContext({
|
||||||
} as DatabaseAccount,
|
databaseAccount: {
|
||||||
});
|
kind: "MongoDB",
|
||||||
|
properties: {
|
||||||
updateConfigContext({
|
ipRules: portalBackendOutboundIPsWithLegacyIPs.map((ip: string) => ({ ipAddressOrRange: ip }) as IpRule),
|
||||||
BACKEND_ENDPOINT: endpoint,
|
publicNetworkAccess: "Enabled",
|
||||||
});
|
},
|
||||||
|
} as DatabaseAccount,
|
||||||
let asyncWarningMessageResult: string;
|
|
||||||
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
|
|
||||||
|
|
||||||
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
|
|
||||||
expect(asyncWarningMessageResult).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: legacyBackendEndpoint,
|
||||||
|
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", () => {
|
it("should return accessMessage when incorrent ip rule is added to mongo/cassandra account per endpoint", async () => {
|
||||||
validEndpoints.forEach(async (endpoint) => {
|
updateUserContext({
|
||||||
updateUserContext({
|
databaseAccount: {
|
||||||
databaseAccount: {
|
kind: "MongoDB",
|
||||||
kind: "MongoDB",
|
properties: {
|
||||||
properties: {
|
ipRules: [{ ipAddressOrRange: "1.1.1.1" }],
|
||||||
ipRules: [{ ipAddressOrRange: "1.1.1.1" }],
|
publicNetworkAccess: "Enabled",
|
||||||
publicNetworkAccess: "Enabled",
|
},
|
||||||
},
|
} as DatabaseAccount,
|
||||||
} as DatabaseAccount,
|
|
||||||
});
|
|
||||||
|
|
||||||
updateConfigContext({
|
|
||||||
BACKEND_ENDPOINT: endpoint,
|
|
||||||
});
|
|
||||||
|
|
||||||
let asyncWarningMessageResult: string;
|
|
||||||
const asyncWarningMessageFunc = (msg: string) => (asyncWarningMessageResult = msg);
|
|
||||||
|
|
||||||
await getNetworkSettingsWarningMessage(asyncWarningMessageFunc);
|
|
||||||
expect(asyncWarningMessageResult).toContain(accessMessagePart);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: legacyBackendEndpoint,
|
||||||
|
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
|
// Postgres and vcore mongo account checks basically pass through to CheckFirewallRules so those
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
import { CassandraProxyEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||||
import { configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { PortalBackendIPs } from "Utils/EndpointUtils";
|
import {
|
||||||
|
CassandraProxyOutboundIPs,
|
||||||
|
MongoProxyOutboundIPs,
|
||||||
|
PortalBackendIPs,
|
||||||
|
PortalBackendOutboundIPs,
|
||||||
|
} from "Utils/EndpointUtils";
|
||||||
|
|
||||||
export const getNetworkSettingsWarningMessage = async (
|
export const getNetworkSettingsWarningMessage = async (
|
||||||
setStateFunc: (warningMessage: string) => void,
|
setStateFunc: (warningMessage: string) => void,
|
||||||
@@ -45,18 +51,53 @@ export const getNetworkSettingsWarningMessage = async (
|
|||||||
const ipRules = accountProperties.ipRules;
|
const ipRules = accountProperties.ipRules;
|
||||||
// public network access is NOT set to "All networks"
|
// public network access is NOT set to "All networks"
|
||||||
if (ipRules?.length > 0) {
|
if (ipRules?.length > 0) {
|
||||||
if (userContext.apiType === "Cassandra" || userContext.apiType === "Mongo") {
|
const isProdOrMpacPortalBackendEndpoint: boolean = [
|
||||||
const portalIPs = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
|
PortalBackendEndpoints.Mpac,
|
||||||
let numberOfMatches = 0;
|
PortalBackendEndpoints.Prod,
|
||||||
ipRules.forEach((ipRule) => {
|
].includes(configContext.PORTAL_BACKEND_ENDPOINT);
|
||||||
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {
|
const portalBackendOutboundIPs: string[] = isProdOrMpacPortalBackendEndpoint
|
||||||
numberOfMatches++;
|
? [
|
||||||
}
|
...PortalBackendOutboundIPs[PortalBackendEndpoints.Mpac],
|
||||||
});
|
...PortalBackendOutboundIPs[PortalBackendEndpoints.Prod],
|
||||||
|
]
|
||||||
|
: PortalBackendOutboundIPs[configContext.PORTAL_BACKEND_ENDPOINT];
|
||||||
|
let portalIPs: string[] = [...portalBackendOutboundIPs, ...PortalBackendIPs[configContext.BACKEND_ENDPOINT]];
|
||||||
|
|
||||||
if (numberOfMatches !== portalIPs.length) {
|
if (userContext.apiType === "Mongo") {
|
||||||
setStateFunc(accessMessage);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,5 +147,33 @@ describe("Query Utils", () => {
|
|||||||
expect(expectedPartitionKeyValues).toContain(documentContent["Type"]);
|
expect(expectedPartitionKeyValues).toContain(documentContent["Type"]);
|
||||||
expect(expectedPartitionKeyValues).toContain(documentContent["Status"]);
|
expect(expectedPartitionKeyValues).toContain(documentContent["Status"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should extract three partition key values even if one is empty", () => {
|
||||||
|
const multiPartitionKeyDefinition: PartitionKeyDefinition = {
|
||||||
|
kind: PartitionKeyKind.MultiHash,
|
||||||
|
paths: ["/Country", "/Region", "/Category"],
|
||||||
|
};
|
||||||
|
const expectedPartitionKeyValues: string[] = ["United States", "US-Washington", ""];
|
||||||
|
const partitioinKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
||||||
|
documentContent,
|
||||||
|
multiPartitionKeyDefinition,
|
||||||
|
);
|
||||||
|
expect(partitioinKeyValues.length).toBe(3);
|
||||||
|
expect(expectedPartitionKeyValues).toContain(documentContent["Country"]);
|
||||||
|
expect(expectedPartitionKeyValues).toContain(documentContent["Region"]);
|
||||||
|
expect(expectedPartitionKeyValues).toContain(documentContent["Category"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract no partition key values in the case nested partition key", () => {
|
||||||
|
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
|
||||||
|
kind: PartitionKeyKind.Hash,
|
||||||
|
paths: ["/Location.type"],
|
||||||
|
};
|
||||||
|
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
||||||
|
documentContent,
|
||||||
|
singlePartitionKeyDefinition,
|
||||||
|
);
|
||||||
|
expect(partitionKeyValues.length).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,18 +2,28 @@ import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
|||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
|
||||||
|
export const defaultQueryFields = ["id", "_self", "_rid", "_ts"];
|
||||||
|
|
||||||
export function buildDocumentsQuery(
|
export function buildDocumentsQuery(
|
||||||
filter: string,
|
filter: string,
|
||||||
partitionKeyProperties: string[],
|
partitionKeyProperties: string[],
|
||||||
partitionKey: DataModels.PartitionKey,
|
partitionKey: DataModels.PartitionKey,
|
||||||
|
additionalField: string[] = [],
|
||||||
): string {
|
): string {
|
||||||
|
const fieldSet = new Set<string>(defaultQueryFields);
|
||||||
|
additionalField.forEach((prop) => fieldSet.add(prop));
|
||||||
|
|
||||||
|
const objectListSpec = [...fieldSet]
|
||||||
|
.filter((f) => !partitionKeyProperties.includes(f))
|
||||||
|
.map((prop) => `c.${prop}`)
|
||||||
|
.join(",");
|
||||||
let query =
|
let query =
|
||||||
partitionKeyProperties && partitionKeyProperties.length > 0
|
partitionKeyProperties && partitionKeyProperties.length > 0
|
||||||
? `select c.id, c._self, c._rid, c._ts, [${buildDocumentsQueryPartitionProjections(
|
? `select ${objectListSpec}, [${buildDocumentsQueryPartitionProjections(
|
||||||
"c",
|
"c",
|
||||||
partitionKey,
|
partitionKey,
|
||||||
)}] as _partitionKeyValue from c`
|
)}] as _partitionKeyValue from c`
|
||||||
: `select c.id, c._self, c._rid, c._ts from c`;
|
: `select ${objectListSpec} from c`;
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
query += " " + filter;
|
query += " " + filter;
|
||||||
@@ -96,7 +106,7 @@ export const extractPartitionKeyValues = (
|
|||||||
const partitionKeyValues: PartitionKey[] = [];
|
const partitionKeyValues: PartitionKey[] = [];
|
||||||
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
|
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
|
||||||
const partitionKeyPathWithoutSlash: string = partitionKeyPath.substring(1);
|
const partitionKeyPathWithoutSlash: string = partitionKeyPath.substring(1);
|
||||||
if (documentContent[partitionKeyPathWithoutSlash]) {
|
if (documentContent[partitionKeyPathWithoutSlash] !== undefined) {
|
||||||
partitionKeyValues.push(documentContent[partitionKeyPathWithoutSlash]);
|
partitionKeyValues.push(documentContent[partitionKeyPathWithoutSlash]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -619,6 +619,31 @@ function shouldForwardMessage(message: PortalMessage, messageOrigin: string) {
|
|||||||
return messageOrigin === window.document.location.origin && message.type === MessageTypes.TelemetryInfo;
|
return messageOrigin === window.document.location.origin && message.type === MessageTypes.TelemetryInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateAADEndpoints(portalEnv: PortalEnv) {
|
||||||
|
switch (portalEnv) {
|
||||||
|
case "prod1":
|
||||||
|
case "prod":
|
||||||
|
updateConfigContext({
|
||||||
|
AAD_ENDPOINT: Constants.AadEndpoints.Prod,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "fairfax":
|
||||||
|
updateConfigContext({
|
||||||
|
AAD_ENDPOINT: Constants.AadEndpoints.Fairfax,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mooncake":
|
||||||
|
updateConfigContext({
|
||||||
|
AAD_ENDPOINT: Constants.AadEndpoints.Mooncake,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown portal environment: ${portalEnv}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||||
if (
|
if (
|
||||||
configContext.BACKEND_ENDPOINT &&
|
configContext.BACKEND_ENDPOINT &&
|
||||||
@@ -639,6 +664,8 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
PORTAL_BACKEND_ENDPOINT: inputs.portalBackendEndpoint,
|
PORTAL_BACKEND_ENDPOINT: inputs.portalBackendEndpoint,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateAADEndpoints(inputs.serverId as PortalEnv);
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
authorizationToken,
|
authorizationToken,
|
||||||
databaseAccount,
|
databaseAccount,
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ApiEndpoints } from "../Common/Constants";
|
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||||
|
import { ApiEndpoints, BackendApi, HttpHeaders } from "../Common/Constants";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import { AccessInputMetadata } from "../Contracts/DataModels";
|
import { AccessInputMetadata } from "../Contracts/DataModels";
|
||||||
|
|
||||||
const url = `${configContext.BACKEND_ENDPOINT}${ApiEndpoints.guestRuntimeProxy}/accessinputmetadata?_=1609359229955`;
|
const url = `${configContext.BACKEND_ENDPOINT}${ApiEndpoints.guestRuntimeProxy}/accessinputmetadata?_=1609359229955`;
|
||||||
|
|
||||||
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
|
export async function fetchAccessData(portalToken: string): Promise<AccessInputMetadata> {
|
||||||
|
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
||||||
|
return fetchAccessData_ToBeDeprecated(portalToken);
|
||||||
|
}
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
// Portal encrypted token API quirk: The token header must be URL encoded
|
// Portal encrypted token API quirk: The token header must be URL encoded
|
||||||
headers.append("x-ms-encrypted-auth-token", encodeURIComponent(portalToken));
|
headers.append(HttpHeaders.guestAccessToken, encodeURIComponent(portalToken));
|
||||||
|
const url: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/accessinputmetadata`;
|
||||||
|
const options = {
|
||||||
|
method: "GET",
|
||||||
|
headers: headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(url, options)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.catch((error) => console.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAccessData_ToBeDeprecated(portalToken: string): Promise<AccessInputMetadata> {
|
||||||
|
const headers = new Headers();
|
||||||
|
// Portal encrypted token API quirk: The token header must be URL encoded
|
||||||
|
headers.append(HttpHeaders.guestAccessToken, encodeURIComponent(portalToken));
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ test("Query stats", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Query errors", async () => {
|
test("Query errors", async () => {
|
||||||
|
test.skip(true, "Disabled due to an issue with error reporting in the backend.");
|
||||||
|
|
||||||
await queryEditor.locator.click();
|
await queryEditor.locator.click();
|
||||||
await queryEditor.setText("SELECT\n glarb(c.id),\n blarg(c.id)\nFROM c");
|
await queryEditor.setText("SELECT\n glarb(c.id),\n blarg(c.id)\nFROM c");
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ import {
|
|||||||
subscriptionId,
|
subscriptionId,
|
||||||
} from "../fx";
|
} from "../fx";
|
||||||
|
|
||||||
test("SQL account using Resource token", async ({ page }) => {
|
// This test is currently failing because of issues with resource token auth in the backend.
|
||||||
|
// Marking it as 'fail' means that it will still be run, but Playwright's reporting will be inverted (failing tests will be marked as passing and vice versa).
|
||||||
|
// This allows us to detect when it starts working again, as the test will fail again.
|
||||||
|
// At that point, we can remove the 'fail' marker and continue running the test as normal.
|
||||||
|
test.fail("SQL account using Resource token", async ({ page }) => {
|
||||||
const credentials = await getAzureCLICredentials();
|
const credentials = await getAzureCLICredentials();
|
||||||
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||||
const accountName = getAccountName(TestAccount.SQL);
|
const accountName = getAccountName(TestAccount.SQL);
|
||||||
@@ -34,9 +38,7 @@ test("SQL account using Resource token", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
await expect(containerPermission).toBeDefined();
|
await expect(containerPermission).toBeDefined();
|
||||||
|
|
||||||
const resourceTokenConnectionString = `AccountEndpoint=${account.documentEndpoint};DatabaseId=${
|
const resourceTokenConnectionString = `AccountEndpoint=${account.documentEndpoint};DatabaseId=${database.id};CollectionId=${container.id};${containerPermission!._token}`;
|
||||||
database.id
|
|
||||||
};CollectionId=${container.id};${containerPermission!._token}`;
|
|
||||||
|
|
||||||
await page.goto("https://localhost:1234/hostedExplorer.html");
|
await page.goto("https://localhost:1234/hostedExplorer.html");
|
||||||
const switchConnectionLink = page.getByTestId("Link:SwitchConnectionType");
|
const switchConnectionLink = page.getByTestId("Link:SwitchConnectionType");
|
||||||
|
|||||||
Reference in New Issue
Block a user