mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-27 13:44:12 +00:00
Compare commits
1 Commits
ashleyst/r
...
ashleyst/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e03287a89e |
@@ -1,16 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm
|
|
||||||
|
|
||||||
# Install pre-reqs for gyp, and 'canvas' npm module
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y \
|
|
||||||
make \
|
|
||||||
gcc \
|
|
||||||
g++ \
|
|
||||||
python3-minimal \
|
|
||||||
libcairo2-dev \
|
|
||||||
libpango1.0-dev \
|
|
||||||
&& \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install node-gyp to build native modules
|
|
||||||
RUN npm install -g node-gyp
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
|
||||||
{
|
|
||||||
"name": "Azure Cosmos DB Explorer",
|
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
|
||||||
"build": {
|
|
||||||
"dockerfile": "Dockerfile"
|
|
||||||
},
|
|
||||||
"onCreateCommand": ".devcontainer/oncreate",
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/azure-cli:1": {
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
|
||||||
"installDirectlyFromGitHubRelease": true,
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/sshd:1": {
|
|
||||||
"version": "latest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
|
||||||
// "features": {},
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// "forwardPorts": [],
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
// "postCreateCommand": "yarn install",
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
// "customizations": {},
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
|
||||||
// "remoteUser": "root"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Install packages once, to prime the node_modules directory.
|
|
||||||
npm ci
|
|
||||||
@@ -1906,14 +1906,8 @@ input::-webkit-calendar-picker-indicator::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
height: 32px;
|
padding-top: 5px;
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
|
|
||||||
.nav-tabs {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navTabHeight {
|
.navTabHeight {
|
||||||
@@ -2618,7 +2612,6 @@ a:link {
|
|||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ a:focus {
|
|||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
width: auto; // Override width: 100% coming from Allotment
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
|
|||||||
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.6",
|
"version": "0.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz",
|
||||||
"integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==",
|
"integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-define-polyfill-provider": "^0.6.2",
|
"@babel/helper-define-polyfill-provider": "^0.6.1",
|
||||||
"core-js-compat": "^3.38.0"
|
"core-js-compat": "^3.36.1"
|
||||||
},
|
},
|
||||||
"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.2",
|
"version": "1.6.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/utils": "^0.2.0"
|
"@floating-ui/utils": "^0.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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.5",
|
"version": "1.6.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/core": "^1.0.0",
|
"@floating-ui/core": "^1.0.0",
|
||||||
"@floating-ui/utils": "^0.2.0"
|
"@floating-ui/utils": "^0.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/utils": {
|
"node_modules/@floating-ui/utils": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"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.28",
|
"@fluentui/react-window-provider": "^2.2.27",
|
||||||
"@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.28",
|
"version": "2.2.27",
|
||||||
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.28.tgz",
|
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.27.tgz",
|
||||||
"integrity": "sha512-YdZ74HTaoDwlvLDzoBST80/17ExIl93tLJpTxnqK5jlJOAUVQ+mxLPF2HQEJq+SZr5IMXHsQ56w/KaZVRn72YA==",
|
"integrity": "sha512-Dg0G9bizjryV0Q/r0CPtCVTPa2II/EsT9E6JT3jPSALjQADDLlW4/+ZXbcEC7geZ/40+KpZDmhplvk/AJSFBKg==",
|
||||||
"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.28",
|
"@fluentui/react-window-provider": "^2.2.27",
|
||||||
"@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.3",
|
"version": "4.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz",
|
||||||
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
|
"integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -14984,9 +14984,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001646",
|
"caniuse-lite": "^1.0.30001640",
|
||||||
"electron-to-chromium": "^1.5.4",
|
"electron-to-chromium": "^1.4.820",
|
||||||
"node-releases": "^2.0.18",
|
"node-releases": "^2.0.14",
|
||||||
"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.30001651",
|
"version": "1.0.30001645",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001645.tgz",
|
||||||
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
|
"integrity": "sha512-GFtY2+qt91kzyMk6j48dJcwJVq5uTkk71XxE3RtScx7XWRLsO7bU44LOFkOZYR8w9YMS0UhPSYpN/6rAMImmLw==",
|
||||||
"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.38.0",
|
"version": "3.37.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
|
||||||
"integrity": "sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==",
|
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.23.3"
|
"browserslist": "^4.23.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ export class BackendApi {
|
|||||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
||||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
||||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
||||||
public static readonly SampleData: string = "SampleData";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
export class PortalBackendEndpoints {
|
||||||
@@ -293,7 +292,6 @@ export class HttpStatusCodes {
|
|||||||
public static readonly Accepted: number = 202;
|
public static readonly Accepted: number = 202;
|
||||||
public static readonly NoContent: number = 204;
|
public static readonly NoContent: number = 204;
|
||||||
public static readonly NotModified: number = 304;
|
public static readonly NotModified: number = 304;
|
||||||
public static readonly BadRequest: number = 400;
|
|
||||||
public static readonly Unauthorized: number = 401;
|
public static readonly Unauthorized: number = 401;
|
||||||
public static readonly Forbidden: number = 403;
|
public static readonly Forbidden: number = 403;
|
||||||
public static readonly NotFound: number = 404;
|
public static readonly NotFound: number = 404;
|
||||||
@@ -505,7 +503,7 @@ export class PriorityLevel {
|
|||||||
public static readonly Default = "low";
|
public static readonly Default = "low";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
|
||||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||||
|
|
||||||
export const QueryCopilotSampleContainerSchema = {
|
export const QueryCopilotSampleContainerSchema = {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { PortalBackendEndpoints } from "Common/Constants";
|
|
||||||
import { updateConfigContext } from "ConfigContext";
|
|
||||||
import * as EnvironmentUtility from "./EnvironmentUtility";
|
import * as EnvironmentUtility from "./EnvironmentUtility";
|
||||||
|
|
||||||
describe("Environment Utility Test", () => {
|
describe("Environment Utility Test", () => {
|
||||||
@@ -13,18 +11,4 @@ describe("Environment Utility Test", () => {
|
|||||||
const expectedResult = "test/";
|
const expectedResult = "test/";
|
||||||
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult);
|
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Detect environment is Mpac", () => {
|
|
||||||
updateConfigContext({
|
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
|
||||||
});
|
|
||||||
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Mpac);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Detect environment is Development", () => {
|
|
||||||
updateConfigContext({
|
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development,
|
|
||||||
});
|
|
||||||
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Development);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,29 +1,6 @@
|
|||||||
import { PortalBackendEndpoints } from "Common/Constants";
|
|
||||||
import { configContext } from "ConfigContext";
|
|
||||||
|
|
||||||
export function normalizeArmEndpoint(uri: string): string {
|
export function normalizeArmEndpoint(uri: string): string {
|
||||||
if (uri && uri.slice(-1) !== "/") {
|
if (uri && uri.slice(-1) !== "/") {
|
||||||
return `${uri}/`;
|
return `${uri}/`;
|
||||||
}
|
}
|
||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Environment {
|
|
||||||
Development = "Development",
|
|
||||||
Mpac = "MPAC",
|
|
||||||
Prod = "Prod",
|
|
||||||
Fairfax = "Fairfax",
|
|
||||||
Mooncake = "Mooncake",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getEnvironment = (): Environment => {
|
|
||||||
const environmentMap: { [key: string]: Environment } = {
|
|
||||||
[PortalBackendEndpoints.Development]: Environment.Development,
|
|
||||||
[PortalBackendEndpoints.Mpac]: Environment.Mpac,
|
|
||||||
[PortalBackendEndpoints.Prod]: Environment.Prod,
|
|
||||||
[PortalBackendEndpoints.Fairfax]: Environment.Fairfax,
|
|
||||||
[PortalBackendEndpoints.Mooncake]: Environment.Mooncake,
|
|
||||||
};
|
|
||||||
|
|
||||||
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -723,19 +723,21 @@ export function useMongoProxyEndpoint(api: string): boolean {
|
|||||||
MongoProxyEndpoints.Fairfax,
|
MongoProxyEndpoints.Fairfax,
|
||||||
MongoProxyEndpoints.Mooncake,
|
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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThrottlingError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This function throws most of the time except on Forbidden which is a bit strange
|
// TODO: This function throws most of the time except on Forbidden which is a bit strange
|
||||||
// It causes problems for TypeScript understanding the types
|
// It causes problems for TypeScript understanding the types
|
||||||
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
||||||
@@ -745,14 +747,6 @@ async function errorHandling(response: Response, action: string, params: unknown
|
|||||||
if (response.status === HttpStatusCodes.Forbidden) {
|
if (response.status === HttpStatusCodes.Forbidden) {
|
||||||
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
|
||||||
return;
|
return;
|
||||||
} else if (
|
|
||||||
response.status === HttpStatusCodes.BadRequest &&
|
|
||||||
errorMessage.includes("Error=16500") &&
|
|
||||||
errorMessage.includes("RetryAfterMs=")
|
|
||||||
) {
|
|
||||||
// If throttling is happening, Cosmos DB will return a 400 with a body of:
|
|
||||||
// A write operation resulted in an error. Error=16500, RetryAfterMs=4, Details='Batch write error.
|
|
||||||
throw new ThrottlingError(errorMessage);
|
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/Common/PortalNotifications.ts
Normal file
39
src/Common/PortalNotifications.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { configContext, Platform } from "../ConfigContext";
|
||||||
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
import { userContext } from "../UserContext";
|
||||||
|
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||||
|
|
||||||
|
const notificationsPath = () => {
|
||||||
|
switch (configContext.platform) {
|
||||||
|
case Platform.Hosted:
|
||||||
|
return "/api/guest/notifications";
|
||||||
|
case Platform.Portal:
|
||||||
|
return "/api/notifications";
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown platform: ${configContext.platform}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
||||||
|
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { databaseAccount, resourceGroup, subscriptionId } = userContext;
|
||||||
|
const url = `${configContext.BACKEND_ENDPOINT}${notificationsPath()}?accountName=${
|
||||||
|
databaseAccount.name
|
||||||
|
}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
|
||||||
|
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||||
|
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||||
|
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as DataModels.Notification[];
|
||||||
|
};
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import QueryError, { QueryErrorLocation, QueryErrorSeverity } from "Common/QueryError";
|
|
||||||
|
|
||||||
describe("QueryError.tryParse", () => {
|
|
||||||
const testErrorLocationResolver = ({ start, end }: { start: number; end: number }) =>
|
|
||||||
new QueryErrorLocation(
|
|
||||||
{ offset: start, lineNumber: start, column: start },
|
|
||||||
{ offset: end, lineNumber: end, column: end },
|
|
||||||
);
|
|
||||||
|
|
||||||
it("handles a string error", () => {
|
|
||||||
const error = "error";
|
|
||||||
const result = QueryError.tryParse(error, testErrorLocationResolver);
|
|
||||||
expect(result).toEqual([new QueryError("error", QueryErrorSeverity.Error)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles an error object", () => {
|
|
||||||
const error = {
|
|
||||||
message: "error",
|
|
||||||
severity: "Warning",
|
|
||||||
location: { start: 0, end: 1 },
|
|
||||||
code: "code",
|
|
||||||
};
|
|
||||||
const result = QueryError.tryParse(error, testErrorLocationResolver);
|
|
||||||
expect(result).toEqual([
|
|
||||||
new QueryError(
|
|
||||||
"error",
|
|
||||||
QueryErrorSeverity.Warning,
|
|
||||||
"code",
|
|
||||||
new QueryErrorLocation({ offset: 0, lineNumber: 0, column: 0 }, { offset: 1, lineNumber: 1, column: 1 }),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles a JSON message without syntax errors", () => {
|
|
||||||
const innerError = {
|
|
||||||
code: "BadRequest",
|
|
||||||
message: "Your query is bad, and you should feel bad",
|
|
||||||
};
|
|
||||||
const message = JSON.stringify(innerError);
|
|
||||||
const outerError = {
|
|
||||||
code: "BadRequest",
|
|
||||||
message,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
|
|
||||||
expect(result).toEqual([
|
|
||||||
new QueryError("Your query is bad, and you should feel bad", QueryErrorSeverity.Error, "BadRequest"),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message.
|
|
||||||
it("handles single-nested error", () => {
|
|
||||||
const errors = [
|
|
||||||
{
|
|
||||||
message: "error1",
|
|
||||||
severity: "Warning",
|
|
||||||
location: { start: 0, end: 1 },
|
|
||||||
code: "code1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "error2",
|
|
||||||
severity: "Error",
|
|
||||||
location: { start: 2, end: 3 },
|
|
||||||
code: "code2",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const innerError = {
|
|
||||||
code: "BadRequest",
|
|
||||||
message: "Your query is bad, and you should feel bad",
|
|
||||||
errors,
|
|
||||||
};
|
|
||||||
const message = JSON.stringify(innerError);
|
|
||||||
const outerError = {
|
|
||||||
code: "BadRequest",
|
|
||||||
message,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
|
|
||||||
expect(result).toEqual([
|
|
||||||
new QueryError(
|
|
||||||
"error1",
|
|
||||||
QueryErrorSeverity.Warning,
|
|
||||||
"code1",
|
|
||||||
new QueryErrorLocation({ offset: 0, lineNumber: 0, column: 0 }, { offset: 1, lineNumber: 1, column: 1 }),
|
|
||||||
),
|
|
||||||
new QueryError(
|
|
||||||
"error2",
|
|
||||||
QueryErrorSeverity.Error,
|
|
||||||
"code2",
|
|
||||||
new QueryErrorLocation({ offset: 2, lineNumber: 2, column: 2 }, { offset: 3, lineNumber: 3, column: 3 }),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { getErrorMessage } from "Common/ErrorHandlingUtils";
|
||||||
import { monaco } from "Explorer/LazyMonaco";
|
import { monaco } from "Explorer/LazyMonaco";
|
||||||
import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
|
||||||
|
|
||||||
export enum QueryErrorSeverity {
|
export enum QueryErrorSeverity {
|
||||||
Error = "Error",
|
Error = "Error",
|
||||||
@@ -97,44 +97,13 @@ export const createMonacoMarkersForQueryErrors = (errors: QueryError[]) => {
|
|||||||
.filter((marker) => !!marker);
|
.filter((marker) => !!marker);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ErrorEnrichment {
|
|
||||||
title?: string;
|
|
||||||
message: string;
|
|
||||||
learnMoreUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {
|
|
||||||
OPERATION_RU_LIMIT_EXCEEDED: (original) => {
|
|
||||||
if (ruThresholdEnabled()) {
|
|
||||||
const threshold = getRUThreshold();
|
|
||||||
return `Query exceeded the Request Unit (RU) limit of ${threshold} RUs. You can change this limit in Data Explorer settings.`;
|
|
||||||
}
|
|
||||||
return original;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const HELP_LINKS: Record<string, string> = {
|
|
||||||
OPERATION_RU_LIMIT_EXCEEDED:
|
|
||||||
"https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer#configure-request-unit-threshold",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class QueryError {
|
export default class QueryError {
|
||||||
message: string;
|
|
||||||
helpLink?: string;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
public message: string,
|
||||||
public severity: QueryErrorSeverity,
|
public severity: QueryErrorSeverity,
|
||||||
public code?: string,
|
public code?: string,
|
||||||
public location?: QueryErrorLocation,
|
public location?: QueryErrorLocation,
|
||||||
helpLink?: string,
|
) {}
|
||||||
) {
|
|
||||||
// Automatically replace the message with a more Data Explorer-specific message if we have for this error code.
|
|
||||||
this.message = REPLACEMENT_MESSAGES[code] ? REPLACEMENT_MESSAGES[code](message) : message;
|
|
||||||
|
|
||||||
// Automatically set the help link if we have one for this error code.
|
|
||||||
this.helpLink = helpLink ?? HELP_LINKS[code];
|
|
||||||
}
|
|
||||||
|
|
||||||
getMonacoSeverity(): monaco.MarkerSeverity {
|
getMonacoSeverity(): monaco.MarkerSeverity {
|
||||||
// It's very difficult to use the monaco.MarkerSeverity enum from here, so we'll just use the numbers directly.
|
// It's very difficult to use the monaco.MarkerSeverity enum from here, so we'll just use the numbers directly.
|
||||||
@@ -166,7 +135,7 @@ export default class QueryError {
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = error as string;
|
const errorMessage = getErrorMessage(error as string | Error);
|
||||||
|
|
||||||
// Map some well known messages to richer errors
|
// Map some well known messages to richer errors
|
||||||
const knownError = knownErrors[errorMessage];
|
const knownError = knownErrors[errorMessage];
|
||||||
@@ -191,9 +160,7 @@ export default class QueryError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const severity =
|
const severity =
|
||||||
"severity" in error && typeof error.severity === "string"
|
"severity" in error && typeof error.severity === "string" ? (error.severity as QueryErrorSeverity) : undefined;
|
||||||
? (error.severity as QueryErrorSeverity)
|
|
||||||
: QueryErrorSeverity.Error;
|
|
||||||
const location =
|
const location =
|
||||||
"location" in error && typeof error.location === "object"
|
"location" in error && typeof error.location === "object"
|
||||||
? locationResolver(error.location as { start: number; end: number })
|
? locationResolver(error.location as { start: number; end: number })
|
||||||
@@ -206,15 +173,16 @@ export default class QueryError {
|
|||||||
error: unknown,
|
error: unknown,
|
||||||
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
|
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
|
||||||
): QueryError[] | null {
|
): QueryError[] | null {
|
||||||
let message: string | undefined;
|
if (typeof error === "object" && "message" in error) {
|
||||||
if (typeof error === "object" && "message" in error && typeof error.message === "string") {
|
error = error.message;
|
||||||
message = error.message;
|
}
|
||||||
} else {
|
|
||||||
// Unsupported error format.
|
if (typeof error !== "string") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
||||||
|
let message = error;
|
||||||
if (message.startsWith("Message: ")) {
|
if (message.startsWith("Message: ")) {
|
||||||
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
||||||
// So we use a separate variable to avoid this.
|
// So we use a separate variable to avoid this.
|
||||||
@@ -228,15 +196,12 @@ export default class QueryError {
|
|||||||
try {
|
try {
|
||||||
parsed = JSON.parse(message);
|
parsed = JSON.parse(message);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// The message doesn't contain a nested error.
|
// Not a query error.
|
||||||
return [QueryError.read(error, locationResolver)];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof parsed === "object") {
|
if (typeof parsed === "object" && "errors" in parsed && Array.isArray(parsed.errors)) {
|
||||||
if ("errors" in parsed && Array.isArray(parsed.errors)) {
|
return parsed.errors.map((e) => QueryError.read(e, locationResolver)).filter((e) => e !== null);
|
||||||
return parsed.errors.map((e) => QueryError.read(e, locationResolver)).filter((e) => e !== null);
|
|
||||||
}
|
|
||||||
return [QueryError.read(parsed, locationResolver)];
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import * as React from "react";
|
|||||||
|
|
||||||
export interface TooltipProps {
|
export interface TooltipProps {
|
||||||
children: string;
|
children: string;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children, className }: TooltipProps) => {
|
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }: TooltipProps) => {
|
||||||
return (
|
return (
|
||||||
<span className={className}>
|
<span>
|
||||||
<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>
|
||||||
|
|||||||
@@ -26,23 +26,14 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IBulkDeleteResult {
|
|
||||||
documentId: DocumentId;
|
|
||||||
requestCharge: number;
|
|
||||||
statusCode: number;
|
|
||||||
retryAfterMilliseconds?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bulk delete documents
|
* Bulk delete documents
|
||||||
* @param collection
|
* @param collection
|
||||||
* @param documentId
|
* @param documentId
|
||||||
* @returns array of results and status codes
|
* @returns array of ids that were successfully deleted
|
||||||
*/
|
*/
|
||||||
export const deleteDocuments = async (
|
export const deleteDocuments = async (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> => {
|
||||||
collection: CollectionBase,
|
const nbDocuments = documentIds.length;
|
||||||
documentIds: DocumentId[],
|
|
||||||
): Promise<IBulkDeleteResult[]> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
||||||
try {
|
try {
|
||||||
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
||||||
@@ -65,17 +56,18 @@ export const deleteDocuments = async (
|
|||||||
operationType: BulkOperationType.Delete,
|
operationType: BulkOperationType.Delete,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
|
const promise = v2Container.items.bulk(operations).then((bulkResult) => {
|
||||||
return bulkResults.map((bulkResult, index) => {
|
return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204);
|
||||||
const documentId = documentIdsChunk[index];
|
|
||||||
return { ...bulkResult, documentId };
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
promiseArray.push(promise);
|
promiseArray.push(promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allResult = await Promise.all(promiseArray);
|
const allResult = await Promise.all(promiseArray);
|
||||||
const flatAllResult = Array.prototype.concat.apply([], allResult);
|
const flatAllResult = Array.prototype.concat.apply([], allResult);
|
||||||
|
logConsoleInfo(
|
||||||
|
`Successfully deleted ${getEntityName(flatAllResult.length > 1)}: ${flatAllResult.length} out of ${nbDocuments}`,
|
||||||
|
);
|
||||||
|
// TODO: handle case result.length != nbDocuments
|
||||||
return flatAllResult;
|
return flatAllResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(
|
handleError(
|
||||||
|
|||||||
@@ -49,12 +49,14 @@ export interface ConfigContext {
|
|||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
BACKEND_ENDPOINT?: string;
|
BACKEND_ENDPOINT?: string;
|
||||||
PORTAL_BACKEND_ENDPOINT: string;
|
PORTAL_BACKEND_ENDPOINT?: string;
|
||||||
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;
|
||||||
@@ -85,7 +87,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/",
|
||||||
@@ -115,10 +117,12 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
"deleteDocument",
|
"deleteDocument",
|
||||||
"createCollectionWithProxy",
|
"createCollectionWithProxy",
|
||||||
"legacyMongoShell",
|
"legacyMongoShell",
|
||||||
// "bulkdelete",
|
"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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export interface Database extends TreeNode {
|
|||||||
openAddCollection(database: Database, event: MouseEvent): void;
|
openAddCollection(database: Database, event: MouseEvent): void;
|
||||||
onSettingsClick: () => void;
|
onSettingsClick: () => void;
|
||||||
loadOffer(): Promise<void>;
|
loadOffer(): Promise<void>;
|
||||||
|
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionBase extends TreeNode {
|
export interface CollectionBase extends TreeNode {
|
||||||
@@ -190,6 +191,8 @@ export interface Collection extends CollectionBase {
|
|||||||
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
||||||
|
|
||||||
|
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* React component for Command button component.
|
* React component for Command button component.
|
||||||
*/
|
*/
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||||
|
import { KeyCodes } from "../../../Common/Constants";
|
||||||
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import * as StringUtils from "../../../Utils/StringUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for this component
|
* Options for this component
|
||||||
*/
|
*/
|
||||||
export interface CommandButtonComponentProps {
|
export interface CommandButtonComponentProps {
|
||||||
|
/**
|
||||||
|
* font icon name for the button
|
||||||
|
*/
|
||||||
|
iconName?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* image source for the button icon
|
* image source for the button icon
|
||||||
*/
|
*/
|
||||||
@@ -22,7 +31,7 @@ export interface CommandButtonComponentProps {
|
|||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
*/
|
*/
|
||||||
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent, container: Explorer) => void;
|
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
@@ -111,3 +120,157 @@ export interface CommandButtonComponentProps {
|
|||||||
*/
|
*/
|
||||||
keyboardAction?: KeyboardAction;
|
keyboardAction?: KeyboardAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
||||||
|
private dropdownElt: HTMLElement;
|
||||||
|
private expandButtonElt: HTMLElement;
|
||||||
|
|
||||||
|
public componentDidUpdate(): void {
|
||||||
|
if (!this.dropdownElt || !this.expandButtonElt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyPress(event: React.KeyboardEvent): boolean {
|
||||||
|
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
|
||||||
|
this.commandClickCallback && this.commandClickCallback(event);
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onLauncherKeyDown(event: React.KeyboardEvent<HTMLDivElement>): boolean {
|
||||||
|
if (event.keyCode === KeyCodes.DownArrow) {
|
||||||
|
$(this.dropdownElt).hide();
|
||||||
|
$(this.dropdownElt).show().focus();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (event.keyCode === KeyCodes.UpArrow) {
|
||||||
|
$(this.dropdownElt).hide();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCommandButtonId(): string {
|
||||||
|
if (this.props.id) {
|
||||||
|
return this.props.id;
|
||||||
|
} else {
|
||||||
|
return `commandButton-${StringUtils.stripSpacesFromString(this.props.commandButtonLabel)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static renderButton(options: CommandButtonComponentProps, key?: string): JSX.Element {
|
||||||
|
return <CommandButtonComponent key={key} {...options} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
private commandClickCallback(e: React.SyntheticEvent): void {
|
||||||
|
if (this.props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Query component's parent, not document
|
||||||
|
const el = document.querySelector(".commandDropdownContainer") as HTMLElement;
|
||||||
|
if (el) {
|
||||||
|
el.style.display = "none";
|
||||||
|
}
|
||||||
|
this.props.onCommandClick(e);
|
||||||
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
|
commandButtonClicked: this.props.commandButtonLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderChildren(): JSX.Element {
|
||||||
|
if (!this.props.children || this.props.children.length < 1) {
|
||||||
|
return <React.Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="commandExpand"
|
||||||
|
tabIndex={0}
|
||||||
|
ref={(ref: HTMLElement) => {
|
||||||
|
this.expandButtonElt = ref;
|
||||||
|
}}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => this.onLauncherKeyDown(e)}
|
||||||
|
>
|
||||||
|
<div className="commandDropdownLauncher">
|
||||||
|
<span className="partialSplitter" />
|
||||||
|
<span className="expandDropdown">
|
||||||
|
<img src={CollapseChevronDownIcon} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="commandDropdownContainer"
|
||||||
|
ref={(ref: HTMLElement) => {
|
||||||
|
this.dropdownElt = ref;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="commandDropdown">
|
||||||
|
{this.props.children.map((c: CommandButtonComponentProps, index: number): JSX.Element => {
|
||||||
|
return CommandButtonComponent.renderButton(c, `${index}`);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static renderLabel(
|
||||||
|
props: CommandButtonComponentProps,
|
||||||
|
key?: string,
|
||||||
|
refct?: (input: HTMLElement) => void,
|
||||||
|
): JSX.Element {
|
||||||
|
if (!props.commandButtonLabel) {
|
||||||
|
return <React.Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="commandLabel" key={key} ref={refct}>
|
||||||
|
{props.commandButtonLabel}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
let mainClassName = "commandButtonComponent";
|
||||||
|
if (this.props.disabled) {
|
||||||
|
mainClassName += " commandDisabled";
|
||||||
|
}
|
||||||
|
if (this.props.isSelected) {
|
||||||
|
mainClassName += " selectedButton";
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentClassName = "commandContent";
|
||||||
|
if (this.props.children && this.props.children.length > 0) {
|
||||||
|
contentClassName += " hasHiddenItems";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="commandButtonReact">
|
||||||
|
<span
|
||||||
|
className={mainClassName}
|
||||||
|
role="menuitem"
|
||||||
|
tabIndex={this.props.tabIndex}
|
||||||
|
onKeyPress={(e: React.KeyboardEvent<HTMLSpanElement>) => this.onKeyPress(e)}
|
||||||
|
title={this.props.tooltipText}
|
||||||
|
id={this.getCommandButtonId()}
|
||||||
|
aria-disabled={this.props.disabled}
|
||||||
|
aria-haspopup={this.props.hasPopup}
|
||||||
|
aria-label={this.props.ariaLabel}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLSpanElement>) => this.commandClickCallback(e)}
|
||||||
|
>
|
||||||
|
<div className={contentClassName}>
|
||||||
|
<img className="commandIcon" src={this.props.iconSrc} alt={this.props.iconAlt} />
|
||||||
|
{CommandButtonComponent.renderLabel(this.props)}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
{this.props.children && this.renderChildren()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface DialogState {
|
|||||||
textFieldProps?: TextFieldProps,
|
textFieldProps?: TextFieldProps,
|
||||||
primaryButtonDisabled?: boolean,
|
primaryButtonDisabled?: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => void;
|
showOkModalDialog: (title: string, subText: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||||
@@ -83,7 +83,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
|||||||
textFieldProps,
|
textFieldProps,
|
||||||
primaryButtonDisabled,
|
primaryButtonDisabled,
|
||||||
}),
|
}),
|
||||||
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps): void =>
|
showOkModalDialog: (title: string, subText: string): void =>
|
||||||
get().openDialog({
|
get().openDialog({
|
||||||
isModal: true,
|
isModal: true,
|
||||||
title,
|
title,
|
||||||
@@ -94,7 +94,6 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
|||||||
get().closeDialog();
|
get().closeDialog();
|
||||||
},
|
},
|
||||||
onSecondaryButtonClick: undefined,
|
onSecondaryButtonClick: undefined,
|
||||||
linkProps,
|
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogBody,
|
|
||||||
DialogContent,
|
|
||||||
DialogSurface,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
Field,
|
|
||||||
ProgressBar,
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
interface ProgressModalDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
maxValue: number;
|
|
||||||
value: number;
|
|
||||||
dismissText: string;
|
|
||||||
onDismiss: () => void;
|
|
||||||
onCancel?: () => void;
|
|
||||||
/* mode drives the state of the action buttons
|
|
||||||
* inProgress: Show cancel button
|
|
||||||
* completed: Show close button
|
|
||||||
* aborting: Show cancel button, but disabled
|
|
||||||
* aborted: Show close button
|
|
||||||
*/
|
|
||||||
mode?: "inProgress" | "completed" | "aborting" | "aborted";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React component that renders a modal dialog with a progress bar.
|
|
||||||
*/
|
|
||||||
export const ProgressModalDialog: React.FC<ProgressModalDialogProps> = ({
|
|
||||||
isOpen,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
maxValue,
|
|
||||||
value,
|
|
||||||
dismissText,
|
|
||||||
onCancel,
|
|
||||||
onDismiss,
|
|
||||||
children,
|
|
||||||
mode = "completed",
|
|
||||||
}) => (
|
|
||||||
<Dialog
|
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={(event, data) => {
|
|
||||||
if (!data.open) {
|
|
||||||
onDismiss();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogSurface>
|
|
||||||
<DialogBody>
|
|
||||||
<DialogTitle>{title}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Field validationMessage={message} validationState="none">
|
|
||||||
<ProgressBar max={maxValue} value={value} />
|
|
||||||
</Field>
|
|
||||||
{children}
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
{mode === "inProgress" || mode === "aborting" ? (
|
|
||||||
<Button appearance="secondary" onClick={onCancel} disabled={mode === "aborting"}>
|
|
||||||
{dismissText}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<DialogTrigger disableButtonEnhancement>
|
|
||||||
<Button appearance="primary">Close</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
)}
|
|
||||||
</DialogActions>
|
|
||||||
</DialogBody>
|
|
||||||
</DialogSurface>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
@@ -134,6 +134,7 @@ describe("SettingsComponent", () => {
|
|||||||
readSettings: undefined,
|
readSettings: undefined,
|
||||||
onSettingsClick: undefined,
|
onSettingsClick: undefined,
|
||||||
loadOffer: undefined,
|
loadOffer: undefined,
|
||||||
|
getPendingThroughputSplitNotification: undefined,
|
||||||
} as ViewModels.Database;
|
} as ViewModels.Database;
|
||||||
newCollection.getDatabase = () => newDatabase;
|
newCollection.getDatabase = () => newDatabase;
|
||||||
newCollection.offer = ko.observable(undefined);
|
newCollection.offer = ko.observable(undefined);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
ContainerVectorPolicyComponent,
|
ContainerVectorPolicyComponent,
|
||||||
ContainerVectorPolicyComponentProps,
|
ContainerVectorPolicyComponentProps,
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||||
@@ -33,6 +32,7 @@ import {
|
|||||||
PartitionKeyComponent,
|
PartitionKeyComponent,
|
||||||
PartitionKeyComponentProps,
|
PartitionKeyComponentProps,
|
||||||
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
||||||
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import "./SettingsComponent.less";
|
import "./SettingsComponent.less";
|
||||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||||
@@ -130,6 +130,7 @@ export interface SettingsComponentState {
|
|||||||
conflictResolutionPolicyProcedureBaseline: string;
|
conflictResolutionPolicyProcedureBaseline: string;
|
||||||
isConflictResolutionDirty: boolean;
|
isConflictResolutionDirty: boolean;
|
||||||
|
|
||||||
|
initialNotification: DataModels.Notification;
|
||||||
selectedTab: SettingsV2TabTypes;
|
selectedTab: SettingsV2TabTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +229,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
conflictResolutionPolicyProcedureBaseline: undefined,
|
conflictResolutionPolicyProcedureBaseline: undefined,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
|
|
||||||
|
initialNotification: undefined,
|
||||||
selectedTab: SettingsV2TabTypes.ScaleTab,
|
selectedTab: SettingsV2TabTypes.ScaleTab,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1050,6 +1052,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
|
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
|
||||||
onScaleSaveableChange: this.onScaleSaveableChange,
|
onScaleSaveableChange: this.onScaleSaveableChange,
|
||||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||||
|
initialNotification: this.props.settingsTab.pendingNotification(),
|
||||||
throughputError: this.state.throughputError,
|
throughputError: this.state.throughputError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import ko from "knockout";
|
||||||
|
import React from "react";
|
||||||
import * as Constants from "../../../../Common/Constants";
|
import * as Constants from "../../../../Common/Constants";
|
||||||
|
import * as DataModels from "../../../../Contracts/DataModels";
|
||||||
import { updateUserContext } from "../../../../UserContext";
|
import { updateUserContext } from "../../../../UserContext";
|
||||||
import Explorer from "../../../Explorer";
|
import Explorer from "../../../Explorer";
|
||||||
|
import { throughputUnit } from "../SettingsRenderUtils";
|
||||||
import { collection } from "../TestUtils";
|
import { collection } from "../TestUtils";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
||||||
|
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||||
|
|
||||||
describe("ScaleComponent", () => {
|
describe("ScaleComponent", () => {
|
||||||
|
const targetThroughput = 6000;
|
||||||
|
|
||||||
const baseProps: ScaleComponentProps = {
|
const baseProps: ScaleComponentProps = {
|
||||||
collection: collection,
|
collection: collection,
|
||||||
database: undefined,
|
database: undefined,
|
||||||
@@ -28,8 +36,39 @@ describe("ScaleComponent", () => {
|
|||||||
onScaleDiscardableChange: () => {
|
onScaleDiscardableChange: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
initialNotification: {
|
||||||
|
description: `Throughput update for ${targetThroughput} ${throughputUnit}`,
|
||||||
|
} as DataModels.Notification,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
it("renders with correct initial notification", () => {
|
||||||
|
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||||
|
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
|
||||||
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
|
||||||
|
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(`${targetThroughput}`);
|
||||||
|
|
||||||
|
const newCollection = { ...collection };
|
||||||
|
const maxThroughput = 5000;
|
||||||
|
newCollection.offer = ko.observable({
|
||||||
|
manualThroughput: undefined,
|
||||||
|
autoscaleMaxThroughput: maxThroughput,
|
||||||
|
minimumThroughput: 400,
|
||||||
|
id: "offer",
|
||||||
|
offerReplacePending: true,
|
||||||
|
});
|
||||||
|
const newProps = {
|
||||||
|
...baseProps,
|
||||||
|
initialNotification: undefined as DataModels.Notification,
|
||||||
|
collection: newCollection,
|
||||||
|
};
|
||||||
|
wrapper = shallow(<ScaleComponent {...newProps} />);
|
||||||
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||||
|
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||||
|
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(`${maxThroughput}`);
|
||||||
|
});
|
||||||
|
|
||||||
it("autoScale disabled", () => {
|
it("autoScale disabled", () => {
|
||||||
const scaleComponent = new ScaleComponent(baseProps);
|
const scaleComponent = new ScaleComponent(baseProps);
|
||||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(false);
|
expect(scaleComponent.isAutoScaleEnabled()).toEqual(false);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
|||||||
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
|
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
|
||||||
import {
|
import {
|
||||||
getTextFieldStyles,
|
getTextFieldStyles,
|
||||||
|
getThroughputApplyLongDelayMessage,
|
||||||
getThroughputApplyShortDelayMessage,
|
getThroughputApplyShortDelayMessage,
|
||||||
subComponentStackProps,
|
subComponentStackProps,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
@@ -33,6 +34,7 @@ export interface ScaleComponentProps {
|
|||||||
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
||||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
|
initialNotification: DataModels.Notification;
|
||||||
throughputError?: string;
|
throughputError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +102,10 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public getInitialNotificationElement = (): JSX.Element => {
|
public getInitialNotificationElement = (): JSX.Element => {
|
||||||
|
if (this.props.initialNotification) {
|
||||||
|
return this.getLongDelayMessage();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.offer?.offerReplacePending) {
|
if (this.offer?.offerReplacePending) {
|
||||||
const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
|
const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
|
||||||
return getThroughputApplyShortDelayMessage(
|
return getThroughputApplyShortDelayMessage(
|
||||||
@@ -114,6 +120,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getLongDelayMessage = (): JSX.Element => {
|
||||||
|
const matches: string[] = this.props.initialNotification?.description.match(
|
||||||
|
`Throughput update for (.*) ${throughputUnit}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const throughput = this.props.throughputBaseline;
|
||||||
|
const targetThroughput: number = matches.length > 1 && Number(matches[1]);
|
||||||
|
if (targetThroughput) {
|
||||||
|
return getThroughputApplyLongDelayMessage(
|
||||||
|
this.props.wasAutopilotOriginallySet,
|
||||||
|
throughput,
|
||||||
|
throughputUnit,
|
||||||
|
this.databaseId,
|
||||||
|
this.collectionId,
|
||||||
|
targetThroughput,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
private getThroughputInputComponent = (): JSX.Element => (
|
private getThroughputInputComponent = (): JSX.Element => (
|
||||||
<ThroughputInputAutoPilotV3Component
|
<ThroughputInputAutoPilotV3Component
|
||||||
databaseAccount={userContext?.databaseAccount}
|
databaseAccount={userContext?.databaseAccount}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledMessageBar
|
||||||
|
messageBarType={5}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
id="throughputApplyLongDelayMessage"
|
||||||
|
styles={
|
||||||
|
{
|
||||||
|
"root": {
|
||||||
|
"color": "windowtext",
|
||||||
|
"fontSize": 14,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
|
||||||
|
<br />
|
||||||
|
Database: test, Container: test
|
||||||
|
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
|
||||||
|
</Text>
|
||||||
|
</StyledMessageBar>
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
{
|
||||||
|
"childrenGap": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ThroughputInputAutoPilotV3Component
|
||||||
|
canExceedMaximumValue={true}
|
||||||
|
collectionName="test"
|
||||||
|
databaseName="test"
|
||||||
|
isAutoPilotSelected={false}
|
||||||
|
isEmulator={false}
|
||||||
|
isEnabled={true}
|
||||||
|
isFixed={false}
|
||||||
|
label="Throughput (6,000 - unlimited RU/s)"
|
||||||
|
maxAutoPilotThroughput={4000}
|
||||||
|
maxAutoPilotThroughputBaseline={4000}
|
||||||
|
maximum={1000000}
|
||||||
|
minimum={6000}
|
||||||
|
onAutoPilotSelected={[Function]}
|
||||||
|
onMaxAutoPilotThroughputChange={[Function]}
|
||||||
|
onScaleDiscardableChange={[Function]}
|
||||||
|
onScaleSaveableChange={[Function]}
|
||||||
|
onThroughputChange={[Function]}
|
||||||
|
spendAckChecked={false}
|
||||||
|
throughput={1000}
|
||||||
|
throughputBaseline={1000}
|
||||||
|
usageSizeInKB={100}
|
||||||
|
wasAutopilotOriginallySet={true}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
@@ -44,6 +44,7 @@ describe("SettingsUtils", () => {
|
|||||||
readSettings: undefined,
|
readSettings: undefined,
|
||||||
onSettingsClick: undefined,
|
onSettingsClick: undefined,
|
||||||
loadOffer: undefined,
|
loadOffer: undefined,
|
||||||
|
getPendingThroughputSplitNotification: undefined,
|
||||||
} as ViewModels.Database;
|
} as ViewModels.Database;
|
||||||
};
|
};
|
||||||
newCollection.offer(undefined);
|
newCollection.offer(undefined);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const useTreeStyles = makeStyles({
|
|||||||
minWidth: "100%",
|
minWidth: "100%",
|
||||||
rowGap: "0px",
|
rowGap: "0px",
|
||||||
paddingTop: "0px",
|
paddingTop: "0px",
|
||||||
[treeIconWidth]: "16px",
|
[treeIconWidth]: "20px",
|
||||||
[leafNodeSpacing]: "24px",
|
[leafNodeSpacing]: "24px",
|
||||||
},
|
},
|
||||||
nodeIcon: {
|
nodeIcon: {
|
||||||
@@ -32,6 +32,7 @@ export const useTreeStyles = makeStyles({
|
|||||||
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,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
|
|
||||||
const onOpenChange = useCallback(
|
const onOpenChange = useCallback(
|
||||||
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
|
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
|
||||||
if (data.type === "Click" && node.onClick) {
|
if (data.type === "Click" && !isBranch && node.onClick) {
|
||||||
node.onClick();
|
node.onClick();
|
||||||
}
|
}
|
||||||
if (!node.isExpanded && data.open && node.onExpanded) {
|
if (!node.isExpanded && data.open && node.onExpanded) {
|
||||||
@@ -119,7 +119,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|||||||
node.onCollapsed?.();
|
node.onCollapsed?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[node, setIsLoading],
|
[isBranch, node, setIsLoading],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMenuOpenChange = useCallback(
|
const onMenuOpenChange = useCallback(
|
||||||
@@ -149,16 +149,15 @@ 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 treeIcon =
|
const expandIcon = isLoading ? (
|
||||||
node.iconSrc === undefined ? undefined : typeof node.iconSrc === "string" ? (
|
<Spinner size="extra-tiny" />
|
||||||
|
) : !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" />
|
||||||
@@ -175,6 +174,7 @@ 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,7 +200,6 @@ 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,20 +10,13 @@ exports[`TreeNodeComponent does not render children if the node is loading 1`] =
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc 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="___1h29e9h_0000000 fz5stix"
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
@@ -163,7 +156,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "branch",
|
"itemType": "branch",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -186,16 +179,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -225,7 +208,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -248,16 +231,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -269,7 +242,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
class="fui-Tree rnv2ez3 ___jy13a00_lpffjy0 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
role="tree"
|
role="tree"
|
||||||
>
|
>
|
||||||
@@ -283,7 +256,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -306,16 +279,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -337,7 +300,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -360,16 +323,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -390,7 +343,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -410,16 +363,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -440,23 +383,16 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc 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 ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -483,16 +419,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -505,7 +431,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
>
|
>
|
||||||
<TreeProvider
|
<TreeProvider
|
||||||
@@ -573,7 +499,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
className="fui-Tree rnv2ez3 ___jy13a00_lpffjy0 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
role="tree"
|
role="tree"
|
||||||
>
|
>
|
||||||
@@ -661,7 +587,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "branch",
|
"itemType": "branch",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -684,16 +610,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -723,7 +639,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -746,16 +662,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -774,23 +680,16 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc 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 ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child1Label"
|
data-test="TreeNode:root/child1Label"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -817,16 +716,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -839,7 +728,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root/child1Label"
|
data-test="Tree:root/child1Label"
|
||||||
>
|
>
|
||||||
<TreeProvider
|
<TreeProvider
|
||||||
@@ -932,7 +821,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "branch",
|
"itemType": "branch",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -955,16 +844,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -994,7 +873,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1017,16 +896,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -1045,23 +914,16 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc 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 ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child2LoadingLabel"
|
data-test="TreeNode:root/child2LoadingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1088,16 +950,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -1187,7 +1039,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
"itemType": "leaf",
|
"itemType": "leaf",
|
||||||
"layoutRef": {
|
"layoutRef": {
|
||||||
"current": <div
|
"current": <div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1207,16 +1059,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -1245,7 +1087,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1265,16 +1107,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -1293,9 +1125,9 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root/child3ExpandingLabel"
|
data-test="TreeNode:root/child3ExpandingLabel"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1304,12 +1136,12 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j 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__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
|
className="fui-TreeItemLayout__expandIcon rh4pu5o"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
@@ -1352,9 +1184,9 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1381,20 +1213,13 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc 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="___1h29e9h_0000000 fz5stix"
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
@@ -1415,20 +1240,13 @@ exports[`TreeNodeComponent renders a node as expandable if it has empty, but def
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc 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="___1h29e9h_0000000 fz5stix"
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
@@ -1495,9 +1313,9 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
|
|||||||
"className": "___1r8p62d_0000000 f1xg1ack f1e31b4d",
|
"className": "___1r8p62d_0000000 f1xg1ack f1e31b4d",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1545,9 +1363,9 @@ exports[`TreeNodeComponent renders a single node 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1574,9 +1392,9 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
@@ -1603,20 +1421,13 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
className="___kqkdor0_ihxn0o0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc 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="___1h29e9h_0000000 fz5stix"
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
@@ -1625,7 +1436,7 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
|
|||||||
</span>
|
</span>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
>
|
>
|
||||||
<TreeNodeComponent
|
<TreeNodeComponent
|
||||||
@@ -1686,20 +1497,13 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
|
className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc 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="___1h29e9h_0000000 fz5stix"
|
className="___1h29e9h_0000000 fz5stix"
|
||||||
@@ -1708,7 +1512,7 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
|
|||||||
</span>
|
</span>
|
||||||
</TreeItemLayout>
|
</TreeItemLayout>
|
||||||
<Tree
|
<Tree
|
||||||
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
|
className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
|
||||||
data-test="Tree:root"
|
data-test="Tree:root"
|
||||||
>
|
>
|
||||||
<TreeNodeComponent
|
<TreeNodeComponent
|
||||||
@@ -1770,9 +1574,9 @@ exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
|
|||||||
>
|
>
|
||||||
<TreeItemLayout
|
<TreeItemLayout
|
||||||
actions={false}
|
actions={false}
|
||||||
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
className="___kqkdor0_ihxn0o0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
|
||||||
data-test="TreeNode:root"
|
data-test="TreeNode:root"
|
||||||
iconBefore={
|
expandIcon={
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import * as msal from "@azure/msal-browser";
|
import * as msal from "@azure/msal-browser";
|
||||||
import { Link } from "@fluentui/react/lib/Link";
|
import { Link } from "@fluentui/react/lib/Link";
|
||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
@@ -48,6 +46,7 @@ import { useTabs } from "../hooks/useTabs";
|
|||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
|
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||||
@@ -1120,7 +1119,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openUploadItemsPane(): void {
|
public openUploadItemsPanePane(): void {
|
||||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
||||||
}
|
}
|
||||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||||
@@ -1179,11 +1178,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async configureCopilot(): Promise<void> {
|
public async configureCopilot(): Promise<void> {
|
||||||
if (
|
if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
|
||||||
userContext.apiType !== "SQL" ||
|
|
||||||
!userContext.subscriptionId ||
|
|
||||||
![Environment.Development, Environment.Mpac, Environment.Prod].includes(getEnvironment())
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const copilotEnabledPromise = getCopilotEnabled();
|
const copilotEnabledPromise = getCopilotEnabled();
|
||||||
|
|||||||
@@ -4,51 +4,83 @@
|
|||||||
* and update any knockout observables passed from the parent.
|
* and update any knockout observables passed from the parent.
|
||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import {
|
|
||||||
createPlatformButtons,
|
|
||||||
createStaticCommandBarButtons,
|
|
||||||
} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory";
|
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import create, { UseStore } from "zustand";
|
||||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
import { Platform, configContext } from "../../../ConfigContext";
|
import { Platform, configContext } from "../../../ConfigContext";
|
||||||
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
|
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||||
import * as CommandBarUtil from "./CommandBarUtil";
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CommandBarStore {
|
||||||
|
contextButtons: CommandButtonComponentProps[];
|
||||||
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
||||||
|
isHidden: boolean;
|
||||||
|
setIsHidden: (isHidden: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
||||||
|
contextButtons: [],
|
||||||
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||||
|
isHidden: false,
|
||||||
|
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
||||||
|
}));
|
||||||
|
|
||||||
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||||
const selectedNodeState = useSelectedNode();
|
const selectedNodeState = useSelectedNode();
|
||||||
const contextButtons = useCommandBar((state) => state.contextButtons);
|
const buttons = useCommandBar((state) => state.contextButtons);
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
const isHidden = useCommandBar((state) => state.isHidden);
|
||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||||
const staticButtons = createStaticCommandBarButtons(selectedNodeState);
|
|
||||||
const platformButtons = createPlatformButtons();
|
|
||||||
|
|
||||||
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor, container);
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
if (contextButtons?.length > 0) {
|
const buttons =
|
||||||
|
userContext.apiType === "Postgres"
|
||||||
|
? CommandBarComponentButtonFactory.createPostgreButtons(container)
|
||||||
|
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
|
||||||
|
return (
|
||||||
|
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
||||||
|
<FluentCommandBar
|
||||||
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
|
items={CommandBarUtil.convertButton(buttons, backgroundColor)}
|
||||||
|
styles={{
|
||||||
|
root: { backgroundColor: backgroundColor },
|
||||||
|
}}
|
||||||
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
|
||||||
|
const contextButtons = (buttons || []).concat(
|
||||||
|
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
|
||||||
|
);
|
||||||
|
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
|
||||||
|
|
||||||
|
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
|
||||||
|
if (buttons && buttons.length > 0) {
|
||||||
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(
|
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
|
||||||
contextButtons || [],
|
|
||||||
backgroundColor,
|
|
||||||
container,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (uiFabricTabsButtons.length > 0) {
|
if (uiFabricTabsButtons.length > 0) {
|
||||||
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricPlatformButtons = CommandBarUtil.convertButton(platformButtons || [], backgroundColor, container);
|
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||||
uiFabricPlatformButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
|
|
||||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
|
|
||||||
@@ -56,7 +88,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
|
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
|
||||||
connectionInfo?.status !== ConnectionStatusType.Connect
|
connectionInfo?.status !== ConnectionStatusType.Connect
|
||||||
) {
|
) {
|
||||||
uiFabricPlatformButtons.unshift(
|
uiFabricControlButtons.unshift(
|
||||||
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
|
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -75,8 +107,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
|
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||||
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons, container);
|
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||||
setKeyboardHandlers(keyboardHandlers);
|
setKeyboardHandlers(keyboardHandlers);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,7 +116,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||||
farItems={uiFabricPlatformButtons}
|
farItems={uiFabricControlButtons}
|
||||||
styles={rootStyle}
|
styles={rootStyle}
|
||||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { AuthType } from "../../../AuthType";
|
|||||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||||
import { updateUserContext } from "../../../UserContext";
|
import { updateUserContext } from "../../../UserContext";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||||
|
|
||||||
describe("CommandBarComponentButtonFactory tests", () => {
|
describe("CommandBarComponentButtonFactory tests", () => {
|
||||||
|
let mockExplorer: Explorer;
|
||||||
|
|
||||||
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
|
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
|
||||||
|
|
||||||
describe("Enable Azure Synapse Link Button", () => {
|
describe("Enable Azure Synapse Link Button", () => {
|
||||||
@@ -16,6 +19,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -26,7 +30,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Button should be visible", () => {
|
it("Button should be visible", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -42,7 +46,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -58,7 +62,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -71,6 +75,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -103,7 +108,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
},
|
},
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -113,13 +118,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
portalEnv: "mooncake",
|
portalEnv: "mooncake",
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -129,8 +134,12 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const openPostgresShellButtonLabel = "Open PSQL shell";
|
const openPostgresShellButtonLabel = "Open PSQL shell";
|
||||||
const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell";
|
const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell";
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
|
});
|
||||||
|
|
||||||
it("creates Postgres shell button", () => {
|
it("creates Postgres shell button", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createPostgreButtons();
|
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(mockExplorer);
|
||||||
const openPostgresShellButton = buttons.find(
|
const openPostgresShellButton = buttons.find(
|
||||||
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
|
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
|
||||||
);
|
);
|
||||||
@@ -138,7 +147,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("creates vCore Mongo shell button", () => {
|
it("creates vCore Mongo shell button", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons();
|
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(mockExplorer);
|
||||||
const openVCoreMongoShellButton = buttons.find(
|
const openVCoreMongoShellButton = buttons.find(
|
||||||
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
|
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
|
||||||
);
|
);
|
||||||
@@ -153,6 +162,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
mockExplorer = {} as Explorer;
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
authType: AuthType.ResourceToken,
|
authType: AuthType.ResourceToken,
|
||||||
});
|
});
|
||||||
@@ -164,7 +175,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
kind: "DocumentDB",
|
kind: "DocumentDB",
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
expect(buttons.length).toBe(2);
|
expect(buttons.length).toBe(2);
|
||||||
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
||||||
expect(buttons[0].disabled).toBe(false);
|
expect(buttons[0].disabled).toBe(false);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { userContext } from "../../../UserContext";
|
|||||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { OpenFullScreen } from "../../OpenFullScreen";
|
import { OpenFullScreen } from "../../OpenFullScreen";
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
@@ -31,20 +32,19 @@ import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
|
|||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
|
export function createStaticCommandBarButtons(
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
container: Explorer,
|
||||||
return userContext.apiType === "Postgres" ? createPostgreButtons() : createVCoreMongoButtons();
|
selectedNodeState: SelectedNodeState,
|
||||||
}
|
): CommandButtonComponentProps[] {
|
||||||
|
|
||||||
if (userContext.authType === AuthType.ResourceToken) {
|
if (userContext.authType === AuthType.ResourceToken) {
|
||||||
return createStaticCommandBarButtonsForResourceToken(selectedNodeState);
|
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
// Avoid starting with a divider
|
// Avoid starting with a divider
|
||||||
const addDivider = () => {
|
const addDivider = () => {
|
||||||
if (buttons.length > 0 && !buttons[buttons.length - 1].isDivider) {
|
if (buttons.length > 0) {
|
||||||
buttons.push(createDivider());
|
buttons.push(createDivider());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables" &&
|
||||||
userContext.apiType !== "Cassandra"
|
userContext.apiType !== "Cassandra"
|
||||||
) {
|
) {
|
||||||
const addSynapseLink = createOpenSynapseLinkDialogButton();
|
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||||
if (addSynapseLink) {
|
if (addSynapseLink) {
|
||||||
addDivider();
|
addDivider();
|
||||||
buttons.push(addSynapseLink);
|
buttons.push(addSynapseLink);
|
||||||
@@ -67,9 +67,9 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buttonProps = createLoginForEntraIDButton();
|
const buttonProps = createLoginForEntraIDButton(container);
|
||||||
setLoginButtonProps(buttonProps);
|
setLoginButtonProps(buttonProps);
|
||||||
}, [dataPlaneRbacEnabled, aadTokenUpdated]);
|
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
|
||||||
|
|
||||||
if (loginButtonProps) {
|
if (loginButtonProps) {
|
||||||
addDivider();
|
addDivider();
|
||||||
@@ -87,8 +87,8 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
||||||
const openQueryBtn = createOpenQueryButton();
|
const openQueryBtn = createOpenQueryButton(container);
|
||||||
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||||
buttons.push(openQueryBtn);
|
buttons.push(openQueryBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,6 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
tooltipText: userContext.features.commandBarV2 ? "New..." : label,
|
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
disabled:
|
disabled:
|
||||||
@@ -116,12 +115,21 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createContextCommandBarButtons(
|
||||||
|
container: Explorer,
|
||||||
|
selectedNodeState: SelectedNodeState,
|
||||||
|
): CommandButtonComponentProps[] {
|
||||||
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||||
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
||||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => {
|
onCommandClick: () => {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
if (useNotebook.getState().isShellEnabled) {
|
if (useNotebook.getState().isShellEnabled) {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||||
@@ -133,7 +141,6 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
};
|
};
|
||||||
addDivider();
|
|
||||||
buttons.push(newMongoShellBtn);
|
buttons.push(newMongoShellBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,27 +153,25 @@ export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeSta
|
|||||||
const newCassandraShellButton: CommandButtonComponentProps = {
|
const newCassandraShellButton: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => {
|
onCommandClick: () => {
|
||||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
};
|
};
|
||||||
addDivider();
|
|
||||||
buttons.push(newCassandraShellButton);
|
buttons.push(newCassandraShellButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPlatformButtons(): CommandButtonComponentProps[] {
|
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] = [
|
const buttons: CommandButtonComponentProps[] = [
|
||||||
{
|
{
|
||||||
iconSrc: SettingsIcon,
|
iconSrc: SettingsIcon,
|
||||||
iconAlt: "Settings",
|
iconAlt: "Settings",
|
||||||
onCommandClick: (_, container) =>
|
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
||||||
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: "Settings",
|
ariaLabel: "Settings",
|
||||||
tooltipText: "Settings",
|
tooltipText: "Settings",
|
||||||
@@ -202,7 +207,7 @@ export function createPlatformButtons(): CommandButtonComponentProps[] {
|
|||||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||||
iconSrc: FeedbackIcon,
|
iconSrc: FeedbackIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => container.openCESCVAFeedbackBlade(),
|
onCommandClick: () => container.openCESCVAFeedbackBlade(),
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
tooltipText: label,
|
tooltipText: label,
|
||||||
@@ -234,7 +239,7 @@ function areScriptsSupported(): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
|
||||||
if (configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -252,7 +257,7 @@ function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
|||||||
return {
|
return {
|
||||||
iconSrc: SynapseIcon,
|
iconSrc: SynapseIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => container.openEnableSynapseLinkDialog(),
|
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled:
|
disabled:
|
||||||
@@ -261,12 +266,12 @@ function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLoginForEntraIDButton(): CommandButtonComponentProps {
|
function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps {
|
||||||
if (configContext.platform !== Platform.Portal) {
|
if (configContext.platform !== Platform.Portal) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCommandClick: CommandButtonComponentProps["onCommandClick"] = async (_, container) => {
|
const handleCommandClick = async () => {
|
||||||
await container.openLoginForEntraIDPopUp();
|
await container.openLoginForEntraIDPopUp();
|
||||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
};
|
};
|
||||||
@@ -393,14 +398,13 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
|||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenQueryButton(): CommandButtonComponentProps {
|
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
|
||||||
const label = "Open Query";
|
const label = "Open Query";
|
||||||
return {
|
return {
|
||||||
iconSrc: BrowseQueriesIcon,
|
iconSrc: BrowseQueriesIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
tooltipText: userContext.features.commandBarV2 ? "Open Query..." : "Open Query",
|
|
||||||
keyboardAction: KeyboardAction.OPEN_QUERY,
|
keyboardAction: KeyboardAction.OPEN_QUERY,
|
||||||
onCommandClick: (_, container) =>
|
onCommandClick: () =>
|
||||||
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@@ -423,7 +427,10 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind): CommandButtonComponentProps {
|
function createOpenTerminalButtonByKind(
|
||||||
|
container: Explorer,
|
||||||
|
terminalKind: ViewModels.TerminalKind,
|
||||||
|
): CommandButtonComponentProps {
|
||||||
const terminalFriendlyName = (): string => {
|
const terminalFriendlyName = (): string => {
|
||||||
switch (terminalKind) {
|
switch (terminalKind) {
|
||||||
case ViewModels.TerminalKind.Cassandra:
|
case ViewModels.TerminalKind.Cassandra:
|
||||||
@@ -446,7 +453,7 @@ function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind):
|
|||||||
return {
|
return {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: (_, container) => {
|
onCommandClick: () => {
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
container.openNotebookTerminal(terminalKind);
|
container.openNotebookTerminal(terminalKind);
|
||||||
}
|
}
|
||||||
@@ -460,10 +467,11 @@ function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createStaticCommandBarButtonsForResourceToken(
|
function createStaticCommandBarButtonsForResourceToken(
|
||||||
|
container: Explorer,
|
||||||
selectedNodeState: SelectedNodeState,
|
selectedNodeState: SelectedNodeState,
|
||||||
): CommandButtonComponentProps[] {
|
): CommandButtonComponentProps[] {
|
||||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||||
const openQueryBtn = createOpenQueryButton();
|
const openQueryBtn = createOpenQueryButton(container);
|
||||||
|
|
||||||
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
||||||
const isResourceTokenCollectionNodeSelected: boolean =
|
const isResourceTokenCollectionNodeSelected: boolean =
|
||||||
@@ -476,20 +484,20 @@ function createStaticCommandBarButtonsForResourceToken(
|
|||||||
|
|
||||||
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
||||||
if (!openQueryBtn.disabled) {
|
if (!openQueryBtn.disabled) {
|
||||||
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [newSqlQueryBtn, openQueryBtn];
|
return [newSqlQueryBtn, openQueryBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPostgreButtons(): CommandButtonComponentProps[] {
|
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const openPostgreShellBtn = createOpenTerminalButtonByKind(ViewModels.TerminalKind.Postgres);
|
const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres);
|
||||||
|
|
||||||
return [openPostgreShellBtn];
|
return [openPostgreShellBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createVCoreMongoButtons(): CommandButtonComponentProps[] {
|
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(ViewModels.TerminalKind.VCoreMongo);
|
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
|
||||||
|
|
||||||
return [openVCoreMongoTerminalButton];
|
return [openVCoreMongoTerminalButton];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { ICommandBarItemProps } from "@fluentui/react";
|
import { ICommandBarItemProps } from "@fluentui/react";
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import * as CommandBarUtil from "./CommandBarUtil";
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
|
|
||||||
describe("CommandBarUtil tests", () => {
|
describe("CommandBarUtil tests", () => {
|
||||||
const mockExplorer = {} as Explorer;
|
|
||||||
const createButton = (): CommandButtonComponentProps => {
|
const createButton = (): CommandButtonComponentProps => {
|
||||||
return {
|
return {
|
||||||
iconSrc: "icon",
|
iconSrc: "icon",
|
||||||
@@ -24,7 +22,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
const btn = createButton();
|
const btn = createButton();
|
||||||
const backgroundColor = "backgroundColor";
|
const backgroundColor = "backgroundColor";
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer);
|
const converteds = CommandBarUtil.convertButton([btn], backgroundColor);
|
||||||
expect(converteds.length).toBe(1);
|
expect(converteds.length).toBe(1);
|
||||||
const converted = converteds[0];
|
const converted = converteds[0];
|
||||||
expect(converted.split).toBe(undefined);
|
expect(converted.split).toBe(undefined);
|
||||||
@@ -48,7 +46,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
btn.children.push(child);
|
btn.children.push(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor", mockExplorer);
|
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor");
|
||||||
expect(converteds.length).toBe(1);
|
expect(converteds.length).toBe(1);
|
||||||
const converted = converteds[0];
|
const converted = converteds[0];
|
||||||
expect(converted.split).toBe(true);
|
expect(converted.split).toBe(true);
|
||||||
@@ -64,7 +62,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
btns.push(createButton());
|
btns.push(createButton());
|
||||||
}
|
}
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor", mockExplorer);
|
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor");
|
||||||
const uniqueKeys = converteds
|
const uniqueKeys = converteds
|
||||||
.map((btn: ICommandBarItemProps) => btn.key)
|
.map((btn: ICommandBarItemProps) => btn.key)
|
||||||
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
|
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
|
||||||
@@ -76,10 +74,10 @@ describe("CommandBarUtil tests", () => {
|
|||||||
const backgroundColor = "backgroundColor";
|
const backgroundColor = "backgroundColor";
|
||||||
|
|
||||||
btn.commandButtonLabel = undefined;
|
btn.commandButtonLabel = undefined;
|
||||||
let converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
|
let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
||||||
expect(converted.text).toEqual(btn.tooltipText);
|
expect(converted.text).toEqual(btn.tooltipText);
|
||||||
|
|
||||||
converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
|
converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
||||||
delete btn.commandButtonLabel;
|
delete btn.commandButtonLabel;
|
||||||
expect(converted.text).toEqual(btn.tooltipText);
|
expect(converted.text).toEqual(btn.tooltipText);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,11 +25,7 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
|
|||||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||||
* @param btns
|
* @param btns
|
||||||
*/
|
*/
|
||||||
export const convertButton = (
|
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||||
btns: CommandButtonComponentProps[],
|
|
||||||
backgroundColor: string,
|
|
||||||
container: Explorer,
|
|
||||||
): ICommandBarItemProps[] => {
|
|
||||||
const buttonHeightPx =
|
const buttonHeightPx =
|
||||||
configContext.platform == Platform.Fabric
|
configContext.platform == Platform.Fabric
|
||||||
? StyleConstants.FabricCommandBarButtonHeight
|
? StyleConstants.FabricCommandBarButtonHeight
|
||||||
@@ -58,14 +54,15 @@ export const convertButton = (
|
|||||||
iconProps: {
|
iconProps: {
|
||||||
style: {
|
style: {
|
||||||
width: StyleConstants.CommandBarIconWidth, // 16
|
width: StyleConstants.CommandBarIconWidth, // 16
|
||||||
alignSelf: undefined,
|
alignSelf: btn.iconName ? "baseline" : undefined,
|
||||||
filter: getFilter(btn.disabled),
|
filter: getFilter(btn.disabled),
|
||||||
},
|
},
|
||||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||||
|
iconName: btn.iconName,
|
||||||
},
|
},
|
||||||
onClick: btn.onCommandClick
|
onClick: btn.onCommandClick
|
||||||
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||||
btn.onCommandClick(ev, container);
|
btn.onCommandClick(ev);
|
||||||
let copilotEnabled = false;
|
let copilotEnabled = false;
|
||||||
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
||||||
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
||||||
@@ -138,7 +135,7 @@ export const convertButton = (
|
|||||||
result.split = true;
|
result.split = true;
|
||||||
|
|
||||||
result.subMenuProps = {
|
result.subMenuProps = {
|
||||||
items: convertButton(btn.children, backgroundColor, container),
|
items: convertButton(btn.children, backgroundColor),
|
||||||
styles: {
|
styles: {
|
||||||
list: {
|
list: {
|
||||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||||
@@ -189,7 +186,7 @@ export const convertButton = (
|
|||||||
option?: IDropdownOption,
|
option?: IDropdownOption,
|
||||||
index?: number,
|
index?: number,
|
||||||
): void => {
|
): void => {
|
||||||
btn.children[index].onCommandClick(event, container);
|
btn.children[index].onCommandClick(event);
|
||||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,17 +237,14 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createKeyboardHandlers(
|
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
|
||||||
allButtons: CommandButtonComponentProps[],
|
|
||||||
container: Explorer,
|
|
||||||
): KeyboardHandlerMap {
|
|
||||||
const handlers: KeyboardHandlerMap = {};
|
const handlers: KeyboardHandlerMap = {};
|
||||||
|
|
||||||
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
function createHandlers(buttons: CommandButtonComponentProps[]) {
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
if (!button.disabled && button.keyboardAction) {
|
if (!button.disabled && button.keyboardAction) {
|
||||||
handlers[button.keyboardAction] = (e) => {
|
handlers[button.keyboardAction] = (e) => {
|
||||||
button.onCommandClick(e, container);
|
button.onCommandClick(e);
|
||||||
|
|
||||||
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
// If the handler is bound, it means the button is visible and enabled, so we should prevent the default action
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
|
||||||
import create, { UseStore } from "zustand";
|
|
||||||
|
|
||||||
export interface CommandBarStore {
|
|
||||||
contextButtons: CommandButtonComponentProps[];
|
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
|
|
||||||
isHidden: boolean;
|
|
||||||
setIsHidden: (isHidden: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
|
||||||
contextButtons: [],
|
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
|
||||||
isHidden: false,
|
|
||||||
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
|
||||||
}));
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
import {
|
|
||||||
makeStyles,
|
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuItem,
|
|
||||||
MenuList,
|
|
||||||
MenuPopover,
|
|
||||||
MenuTrigger,
|
|
||||||
Toolbar,
|
|
||||||
ToolbarButton,
|
|
||||||
ToolbarDivider,
|
|
||||||
ToolbarGroup,
|
|
||||||
Tooltip,
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import {
|
|
||||||
createPlatformButtons,
|
|
||||||
createStaticCommandBarButtons,
|
|
||||||
} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory";
|
|
||||||
import { createKeyboardHandlers } from "Explorer/Menus/CommandBar/CommandBarUtil";
|
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
|
||||||
import React, { MouseEventHandler } from "react";
|
|
||||||
|
|
||||||
const useToolbarStyles = makeStyles({
|
|
||||||
toolbar: {
|
|
||||||
height: tokens.layoutRowHeight,
|
|
||||||
justifyContent: "space-between", // Ensures that the two toolbar groups are at opposite ends of the toolbar
|
|
||||||
...cosmosShorthands.borderBottom(),
|
|
||||||
},
|
|
||||||
toolbarGroup: {
|
|
||||||
display: "flex",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface CommandBarV2Props {
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommandBarV2: React.FC<CommandBarV2Props> = ({ explorer }: CommandBarV2Props) => {
|
|
||||||
const styles = useToolbarStyles();
|
|
||||||
const selectedNodeState = useSelectedNode();
|
|
||||||
const contextButtons = useCommandBar((state) => state.contextButtons);
|
|
||||||
const isHidden = useCommandBar((state) => state.isHidden);
|
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
|
||||||
const staticButtons = createStaticCommandBarButtons(selectedNodeState);
|
|
||||||
const platformButtons = createPlatformButtons();
|
|
||||||
|
|
||||||
if (isHidden) {
|
|
||||||
setKeyboardHandlers({});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
|
|
||||||
const keyboardHandlers = createKeyboardHandlers(allButtons, explorer);
|
|
||||||
setKeyboardHandlers(keyboardHandlers);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CosmosFluentProvider>
|
|
||||||
<Toolbar className={styles.toolbar}>
|
|
||||||
<ToolbarGroup role="presentation" className={styles.toolbarGroup}>
|
|
||||||
{staticButtons.map((button, index) =>
|
|
||||||
renderButton(explorer, button, `static-${index}`, contextButtons?.length > 0),
|
|
||||||
)}
|
|
||||||
{staticButtons.length > 0 && contextButtons?.length > 0 && <ToolbarDivider />}
|
|
||||||
{contextButtons.map((button, index) => renderButton(explorer, button, `context-${index}`, false))}
|
|
||||||
</ToolbarGroup>
|
|
||||||
<ToolbarGroup role="presentation">
|
|
||||||
{platformButtons.map((button, index) => renderButton(explorer, button, `platform-${index}`, true))}
|
|
||||||
</ToolbarGroup>
|
|
||||||
</Toolbar>
|
|
||||||
</CosmosFluentProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// This allows us to migrate individual buttons over to using a JSX.Element for the icon, without requiring us to change them all at once.
|
|
||||||
function renderIcon(iconSrcOrElement: string | JSX.Element, alt?: string): JSX.Element {
|
|
||||||
if (typeof iconSrcOrElement === "string") {
|
|
||||||
return <img src={iconSrcOrElement} alt={alt} />;
|
|
||||||
}
|
|
||||||
return iconSrcOrElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderButton(
|
|
||||||
explorer: Explorer,
|
|
||||||
btn: CommandButtonComponentProps,
|
|
||||||
key: string,
|
|
||||||
iconOnly: boolean,
|
|
||||||
): JSX.Element {
|
|
||||||
if (btn.isDivider) {
|
|
||||||
return <ToolbarDivider key={key} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasChildren = !!btn.children && btn.children.length > 0;
|
|
||||||
const label = btn.commandButtonLabel || btn.tooltipText;
|
|
||||||
const tooltip = btn.tooltipText || (iconOnly ? label : undefined);
|
|
||||||
const onClick: MouseEventHandler | undefined =
|
|
||||||
btn.onCommandClick && !hasChildren ? (e) => btn.onCommandClick(e, explorer) : undefined;
|
|
||||||
|
|
||||||
// We don't know which element will be the top-level element, so just slap a key on all of 'em
|
|
||||||
|
|
||||||
let button = hasChildren ? (
|
|
||||||
<MenuButton key={key} appearance="subtle" aria-label={btn.ariaLabel} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
|
||||||
{!iconOnly && label}
|
|
||||||
</MenuButton>
|
|
||||||
) : (
|
|
||||||
<ToolbarButton key={key} aria-label={btn.ariaLabel} onClick={onClick} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
|
||||||
{!iconOnly && label}
|
|
||||||
</ToolbarButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tooltip) {
|
|
||||||
button = (
|
|
||||||
<Tooltip key={key} content={tooltip} relationship="description" withArrow>
|
|
||||||
{button}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChildren) {
|
|
||||||
button = (
|
|
||||||
<Menu key={key}>
|
|
||||||
<MenuTrigger disableButtonEnhancement>{button}</MenuTrigger>
|
|
||||||
<MenuPopover>
|
|
||||||
<MenuList>{btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))}</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMenuItem(explorer: Explorer, btn: CommandButtonComponentProps, key: string): JSX.Element {
|
|
||||||
const hasChildren = !!btn.children && btn.children.length > 0;
|
|
||||||
const onClick: MouseEventHandler | undefined = btn.onCommandClick
|
|
||||||
? (e) => btn.onCommandClick(e, explorer)
|
|
||||||
: undefined;
|
|
||||||
const item = (
|
|
||||||
<MenuItem key={key} onClick={onClick} icon={renderIcon(btn.iconSrc, btn.iconAlt)}>
|
|
||||||
{btn.commandButtonLabel || btn.tooltipText}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasChildren) {
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<MenuTrigger disableButtonEnhancement>{item}</MenuTrigger>
|
|
||||||
<MenuPopover>
|
|
||||||
<MenuList>{btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))}</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { clear, collectionWasOpened, getItems, Type } from "Explorer/MostRecentActivity/MostRecentActivity";
|
|
||||||
import { observable } from "knockout";
|
import { observable } from "knockout";
|
||||||
|
import { mostRecentActivity } from "./MostRecentActivity";
|
||||||
|
|
||||||
describe("MostRecentActivity", () => {
|
describe("MostRecentActivity", () => {
|
||||||
const accountName = "some account";
|
const accountId = "some account";
|
||||||
|
|
||||||
beforeEach(() => clear(accountName));
|
beforeEach(() => mostRecentActivity.clear(accountId));
|
||||||
|
|
||||||
it("Has no items at first", () => {
|
it("Has no items at first", () => {
|
||||||
expect(getItems(accountName)).toStrictEqual([]);
|
expect(mostRecentActivity.getItems(accountId)).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Can record collections being opened", () => {
|
it("Can record collections being opened", () => {
|
||||||
@@ -18,9 +18,9 @@ describe("MostRecentActivity", () => {
|
|||||||
databaseId,
|
databaseId,
|
||||||
};
|
};
|
||||||
|
|
||||||
collectionWasOpened(accountName, collection);
|
mostRecentActivity.collectionWasOpened(accountId, collection);
|
||||||
|
|
||||||
const activity = getItems(accountName);
|
const activity = mostRecentActivity.getItems(accountId);
|
||||||
expect(activity).toEqual([
|
expect(activity).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
collectionId,
|
collectionId,
|
||||||
@@ -29,24 +29,58 @@ describe("MostRecentActivity", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Does not store duplicate entries", () => {
|
it("Can record notebooks being opened", () => {
|
||||||
const collectionId = "some collection";
|
const name = "some notebook";
|
||||||
const databaseId = "some database";
|
const path = "some path";
|
||||||
const collection = {
|
const notebook = { name, path };
|
||||||
id: observable(collectionId),
|
|
||||||
databaseId,
|
|
||||||
};
|
|
||||||
|
|
||||||
collectionWasOpened(accountName, collection);
|
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
|
||||||
collectionWasOpened(accountName, collection);
|
|
||||||
|
|
||||||
const activity = getItems(accountName);
|
const activity = mostRecentActivity.getItems(accountId);
|
||||||
expect(activity).toEqual([
|
expect(activity).toEqual([expect.objectContaining(notebook)]);
|
||||||
expect.objectContaining({
|
});
|
||||||
type: Type.OpenCollection,
|
|
||||||
collectionId,
|
it("Filters out duplicates", () => {
|
||||||
databaseId,
|
const name = "some notebook";
|
||||||
}),
|
const path = "some path";
|
||||||
]);
|
const notebook = { name, path };
|
||||||
|
const sameNotebook = { name, path };
|
||||||
|
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, sameNotebook);
|
||||||
|
|
||||||
|
const activity = mostRecentActivity.getItems(accountId);
|
||||||
|
expect(activity.length).toEqual(1);
|
||||||
|
expect(activity).toEqual([expect.objectContaining(notebook)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows for multiple accounts", () => {
|
||||||
|
const name = "some notebook";
|
||||||
|
const path = "some path";
|
||||||
|
const notebook = { name, path };
|
||||||
|
|
||||||
|
const anotherNotebook = { name: "Another " + name, path };
|
||||||
|
const anotherAccountId = "Another " + accountId;
|
||||||
|
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
|
||||||
|
mostRecentActivity.notebookWasItemOpened(anotherAccountId, anotherNotebook);
|
||||||
|
|
||||||
|
expect(mostRecentActivity.getItems(accountId)).toEqual([expect.objectContaining(notebook)]);
|
||||||
|
expect(mostRecentActivity.getItems(anotherAccountId)).toEqual([expect.objectContaining(anotherNotebook)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Can store multiple distinct elements, in FIFO order", () => {
|
||||||
|
const name = "some notebook";
|
||||||
|
const path = "some path";
|
||||||
|
const first = { name, path };
|
||||||
|
const second = { name: "Another " + name, path };
|
||||||
|
const third = { name, path: "Another " + path };
|
||||||
|
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, first);
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, second);
|
||||||
|
mostRecentActivity.notebookWasItemOpened(accountId, third);
|
||||||
|
|
||||||
|
const activity = mostRecentActivity.getItems(accountId);
|
||||||
|
expect(activity).toEqual([third, second, first].map(expect.objectContaining));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { AppStateComponentNames, deleteState, loadState, saveState } from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility";
|
||||||
|
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
|
|
||||||
export enum Type {
|
export enum Type {
|
||||||
OpenCollection = "OpenCollection",
|
OpenCollection,
|
||||||
OpenNotebook = "OpenNotebook",
|
OpenNotebook,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenNotebookItem {
|
export interface OpenNotebookItem {
|
||||||
@@ -21,174 +21,158 @@ export interface OpenCollectionItem {
|
|||||||
|
|
||||||
type Item = OpenNotebookItem | OpenCollectionItem;
|
type Item = OpenNotebookItem | OpenCollectionItem;
|
||||||
|
|
||||||
const itemsMaxNumber: number = 5;
|
// Update schemaVersion if you are going to change this interface
|
||||||
|
interface StoredData {
|
||||||
|
schemaVersion: string;
|
||||||
|
itemsMap: { [accountId: string]: Item[] }; // FIFO
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate old data to new AppState
|
* Stores most recent activity
|
||||||
*/
|
*/
|
||||||
const migrateOldData = () => {
|
class MostRecentActivity {
|
||||||
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
private static readonly schemaVersion: string = "2";
|
||||||
const oldDataSchemaVersion: string = "2";
|
private static itemsMaxNumber: number = 5;
|
||||||
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
|
private storedData: StoredData;
|
||||||
if (rawData) {
|
constructor() {
|
||||||
const oldData = JSON.parse(rawData);
|
// Retrieve from local storage
|
||||||
if (oldData.schemaVersion === oldDataSchemaVersion) {
|
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
||||||
const itemsMap: Record<string, Item[]> = oldData.itemsMap;
|
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
|
||||||
Object.keys(itemsMap).forEach((accountId: string) => {
|
|
||||||
const accountName = accountId.split("/").pop();
|
if (!rawData) {
|
||||||
if (accountName) {
|
this.storedData = MostRecentActivity.createEmptyData();
|
||||||
saveState(
|
} else {
|
||||||
{
|
try {
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
this.storedData = JSON.parse(rawData);
|
||||||
globalAccountName: accountName,
|
} catch (e) {
|
||||||
},
|
console.error("Unable to parse stored most recent activity. Use empty data:", rawData);
|
||||||
itemsMap[accountId].map((item) => {
|
this.storedData = MostRecentActivity.createEmptyData();
|
||||||
if ((item.type as unknown as number) === 0) {
|
}
|
||||||
item.type = Type.OpenCollection;
|
|
||||||
} else if ((item.type as unknown as number) === 1) {
|
// If version doesn't match or schema broke, nuke it!
|
||||||
item.type = Type.OpenNotebook;
|
if (
|
||||||
}
|
!this.storedData.hasOwnProperty("schemaVersion") ||
|
||||||
return item;
|
this.storedData["schemaVersion"] !== MostRecentActivity.schemaVersion
|
||||||
}),
|
) {
|
||||||
);
|
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
||||||
}
|
this.storedData = MostRecentActivity.createEmptyData();
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.storedData = MostRecentActivity.createEmptyData();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let p in this.storedData.itemsMap) {
|
||||||
|
this.cleanupItems(p);
|
||||||
|
}
|
||||||
|
this.saveToLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createEmptyData(): StoredData {
|
||||||
|
return {
|
||||||
|
schemaVersion: MostRecentActivity.schemaVersion,
|
||||||
|
itemsMap: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isEmpty(object: any) {
|
||||||
|
return Object.keys(object).length === 0 && object.constructor === Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveToLocalStorage() {
|
||||||
|
if (MostRecentActivity.isEmpty(this.storedData.itemsMap)) {
|
||||||
|
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
||||||
|
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
||||||
|
}
|
||||||
|
// Don't save if empty
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData));
|
||||||
|
}
|
||||||
|
|
||||||
|
private addItem(accountId: string, newItem: Item): void {
|
||||||
|
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
|
||||||
|
// if (!accountId) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Remove duplicate
|
||||||
|
MostRecentActivity.removeDuplicate(newItem, this.storedData.itemsMap[accountId]);
|
||||||
|
|
||||||
|
this.storedData.itemsMap[accountId] = this.storedData.itemsMap[accountId] || [];
|
||||||
|
this.storedData.itemsMap[accountId].unshift(newItem);
|
||||||
|
this.cleanupItems(accountId);
|
||||||
|
this.saveToLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getItems(accountId: string): Item[] {
|
||||||
|
return this.storedData.itemsMap[accountId] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public collectionWasOpened(accountId: string, { id, databaseId }: Pick<CollectionBase, "id" | "databaseId">) {
|
||||||
|
const collectionId = id();
|
||||||
|
this.addItem(accountId, {
|
||||||
|
type: Type.OpenCollection,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public notebookWasItemOpened(accountId: string, { name, path }: Pick<NotebookContentItem, "name" | "path">) {
|
||||||
|
this.addItem(accountId, {
|
||||||
|
type: Type.OpenNotebook,
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(accountId: string): void {
|
||||||
|
delete this.storedData.itemsMap[accountId];
|
||||||
|
this.saveToLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find items by doing strict comparison and remove from array if duplicate is found
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
private static removeDuplicate(item: Item, itemsArray: Item[]): void {
|
||||||
|
if (!itemsArray) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = -1;
|
||||||
|
for (let i = 0; i < itemsArray.length; i++) {
|
||||||
|
const currentItem = itemsArray[i];
|
||||||
|
if (JSON.stringify(currentItem) === JSON.stringify(item)) {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove old data
|
if (index !== -1) {
|
||||||
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
itemsArray.splice(index, 1);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addItem = (accountName: string, newItem: Item): void => {
|
|
||||||
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
|
|
||||||
// if (!accountId) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
let items =
|
|
||||||
(loadState({
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
}) as Item[]) || [];
|
|
||||||
|
|
||||||
// Remove duplicate
|
|
||||||
items = removeDuplicate(newItem, items);
|
|
||||||
|
|
||||||
items.unshift(newItem);
|
|
||||||
items = cleanupItems(items, accountName);
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
},
|
|
||||||
items,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getItems = (accountName: string): Item[] => {
|
|
||||||
if (!accountName) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
(loadState({
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
}) as Item[]) || []
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const collectionWasOpened = (
|
|
||||||
accountName: string,
|
|
||||||
{ id, databaseId }: Pick<CollectionBase, "id" | "databaseId">,
|
|
||||||
) => {
|
|
||||||
if (accountName === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const collectionId = id();
|
|
||||||
addItem(accountName, {
|
|
||||||
type: Type.OpenCollection,
|
|
||||||
databaseId,
|
|
||||||
collectionId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clear = (accountName: string): void => {
|
|
||||||
if (!accountName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteState({
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sort object by key
|
|
||||||
const sortObjectKeys = (unordered: Record<string, unknown>): Record<string, unknown> => {
|
|
||||||
return Object.keys(unordered)
|
|
||||||
.sort()
|
|
||||||
.reduce((obj: Record<string, unknown>, key: string) => {
|
|
||||||
obj[key] = unordered[key];
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find items by doing strict comparison and remove from array if duplicate is found.
|
|
||||||
* Modifies the array.
|
|
||||||
* @param item
|
|
||||||
* @param itemsArray
|
|
||||||
* @returns new array
|
|
||||||
*/
|
|
||||||
const removeDuplicate = (item: Item, itemsArray: Item[]): Item[] => {
|
|
||||||
if (!itemsArray) {
|
|
||||||
return itemsArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: Item[] = [...itemsArray];
|
|
||||||
|
|
||||||
let index = -1;
|
|
||||||
for (let i = 0; i < result.length; i++) {
|
|
||||||
const currentItem = result[i];
|
|
||||||
|
|
||||||
if (
|
|
||||||
JSON.stringify(sortObjectKeys(currentItem as unknown as Record<string, unknown>)) ===
|
|
||||||
JSON.stringify(sortObjectKeys(item as unknown as Record<string, unknown>))
|
|
||||||
) {
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index !== -1) {
|
/**
|
||||||
result.splice(index, 1);
|
* Remove unknown types
|
||||||
|
* Limit items to max number
|
||||||
|
*/
|
||||||
|
private cleanupItems(accountId: string): void {
|
||||||
|
if (!this.storedData.itemsMap.hasOwnProperty(accountId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsArray = this.storedData.itemsMap[accountId]
|
||||||
|
.filter((item) => item.type in Type)
|
||||||
|
.slice(0, MostRecentActivity.itemsMaxNumber);
|
||||||
|
if (itemsArray.length === 0) {
|
||||||
|
delete this.storedData.itemsMap[accountId];
|
||||||
|
} else {
|
||||||
|
this.storedData.itemsMap[accountId] = itemsArray;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
export const mostRecentActivity = new MostRecentActivity();
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove unknown types
|
|
||||||
* Limit items to max number
|
|
||||||
* Modifies the array.
|
|
||||||
*/
|
|
||||||
const cleanupItems = (items: Item[], accountName: string): Item[] => {
|
|
||||||
if (accountName === undefined) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsArray = items.filter((item) => item.type in Type).slice(0, itemsMaxNumber);
|
|
||||||
if (itemsArray.length === 0) {
|
|
||||||
deleteState({
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return itemsArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
migrateOldData();
|
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import {
|
|||||||
IChoiceGroupOption,
|
IChoiceGroupOption,
|
||||||
ISpinButtonStyles,
|
ISpinButtonStyles,
|
||||||
IToggleStyles,
|
IToggleStyles,
|
||||||
|
Icon,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarType,
|
MessageBarType,
|
||||||
Position,
|
Position,
|
||||||
SpinButton,
|
SpinButton,
|
||||||
Toggle,
|
Toggle,
|
||||||
|
TooltipHost,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, makeStyles } from "@fluentui/react-components";
|
import { makeStyles } from "@fluentui/react-components";
|
||||||
import { AuthType } from "AuthType";
|
import { AuthType } from "AuthType";
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { SplitterDirection } from "Common/Splitter";
|
import { SplitterDirection } from "Common/Splitter";
|
||||||
@@ -57,32 +59,6 @@ const useStyles = makeStyles({
|
|||||||
listStyleType: "disc",
|
listStyleType: "disc",
|
||||||
paddingLeft: "20px",
|
paddingLeft: "20px",
|
||||||
},
|
},
|
||||||
container: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
height: "100%",
|
|
||||||
},
|
|
||||||
firstItem: {
|
|
||||||
flex: "1",
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
marginRight: "5px",
|
|
||||||
},
|
|
||||||
headerIcon: {
|
|
||||||
paddingTop: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
settingsSectionContainer: {
|
|
||||||
paddingLeft: "15px",
|
|
||||||
},
|
|
||||||
settingsSectionDescription: {
|
|
||||||
paddingBottom: "10px",
|
|
||||||
fontSize: "12px",
|
|
||||||
},
|
|
||||||
subHeader: {
|
|
||||||
marginRight: "5px",
|
|
||||||
fontSize: "12px",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({
|
export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({
|
||||||
@@ -468,66 +444,82 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...genericPaneProps}>
|
<RightPaneForm {...genericPaneProps}>
|
||||||
<div className={`paneMainContent ${styles.container}`}>
|
<div className="paneMainContent">
|
||||||
<Accordion className={styles.firstItem}>
|
{shouldShowQueryPageOptions && (
|
||||||
{shouldShowQueryPageOptions && (
|
<div className="settingsSection">
|
||||||
<AccordionItem value="1">
|
<div className="settingsSectionPart">
|
||||||
<AccordionHeader>
|
<fieldset>
|
||||||
<div className={styles.header}>Page Options</div>
|
<legend id="pageOptions" className="settingsSectionLabel legendLabel">
|
||||||
</AccordionHeader>
|
Page Options
|
||||||
<AccordionPanel>
|
</legend>
|
||||||
<div className={styles.settingsSectionContainer}>
|
<InfoTooltip>
|
||||||
<div className={styles.settingsSectionDescription}>
|
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
|
||||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as
|
query results per page.
|
||||||
many query results per page.
|
</InfoTooltip>
|
||||||
|
<ChoiceGroup
|
||||||
|
ariaLabelledBy="pageOptions"
|
||||||
|
selectedKey={pageOption}
|
||||||
|
options={pageOptionList}
|
||||||
|
styles={choiceButtonStyles}
|
||||||
|
onChange={handleOnPageOptionChange}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div className="tabs settingsSectionPart">
|
||||||
|
{isCustomPageOptionSelected() && (
|
||||||
|
<div className="tabcontent">
|
||||||
|
<div className="settingsSectionLabel">
|
||||||
|
Query results per page
|
||||||
|
<InfoTooltip>Enter the number of query results that should be shown per page.</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<ChoiceGroup
|
|
||||||
ariaLabelledBy="pageOptions"
|
<SpinButton
|
||||||
selectedKey={pageOption}
|
ariaLabel="Custom query items per page"
|
||||||
options={pageOptionList}
|
value={"" + customItemPerPage}
|
||||||
styles={choiceButtonStyles}
|
onIncrement={(newValue) => {
|
||||||
onChange={handleOnPageOptionChange}
|
setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage);
|
||||||
|
}}
|
||||||
|
onDecrement={(newValue) => setCustomItemPerPage(parseInt(newValue) - 1 || customItemPerPage)}
|
||||||
|
onValidate={(newValue) => setCustomItemPerPage(parseInt(newValue) || customItemPerPage)}
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
className="textfontclr"
|
||||||
|
incrementButtonAriaLabel="Increase value by 1"
|
||||||
|
decrementButtonAriaLabel="Decrease value by 1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`tabs ${styles.settingsSectionContainer}`}>
|
)}
|
||||||
{isCustomPageOptionSelected() && (
|
</div>
|
||||||
<div className="tabcontent">
|
</div>
|
||||||
<div className={styles.settingsSectionDescription}>
|
)}
|
||||||
Query results per page{" "}
|
{userContext.apiType === "SQL" &&
|
||||||
<InfoTooltip className={styles.headerIcon}>
|
userContext.authType === AuthType.AAD &&
|
||||||
Enter the number of query results that should be shown per page.
|
configContext.platform !== Platform.Fabric && (
|
||||||
</InfoTooltip>
|
<>
|
||||||
</div>
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart">
|
||||||
<SpinButton
|
<fieldset>
|
||||||
ariaLabel="Custom query items per page"
|
<legend id="enableDataPlaneRBACOptions" className="settingsSectionLabel legendLabel">
|
||||||
value={"" + customItemPerPage}
|
Enable Entra ID RBAC
|
||||||
onIncrement={(newValue) => {
|
</legend>
|
||||||
setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage);
|
<TooltipHost
|
||||||
}}
|
content={
|
||||||
onDecrement={(newValue) => setCustomItemPerPage(parseInt(newValue) - 1 || customItemPerPage)}
|
<>
|
||||||
onValidate={(newValue) => setCustomItemPerPage(parseInt(newValue) || customItemPerPage)}
|
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable
|
||||||
min={1}
|
Entra ID RBAC.
|
||||||
step={1}
|
<a
|
||||||
className="textfontclr"
|
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||||
incrementButtonAriaLabel="Increase value by 1"
|
target="_blank"
|
||||||
decrementButtonAriaLabel="Decrease value by 1"
|
rel="noopener noreferrer"
|
||||||
/>
|
>
|
||||||
</div>
|
{" "}
|
||||||
)}
|
Learn more{" "}
|
||||||
</div>
|
</a>
|
||||||
</AccordionPanel>
|
</>
|
||||||
</AccordionItem>
|
}
|
||||||
)}
|
>
|
||||||
{userContext.apiType === "SQL" &&
|
<Icon iconName="Info" ariaLabel="Info tooltip" className="panelInfoIcon" tabIndex={0} />
|
||||||
userContext.authType === AuthType.AAD &&
|
</TooltipHost>
|
||||||
configContext.platform !== Platform.Fabric && (
|
|
||||||
<AccordionItem value="2">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
|
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarType={MessageBarType.warning}
|
messageBarType={MessageBarType.warning}
|
||||||
@@ -539,18 +531,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
operations
|
operations
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
|
||||||
ID RBAC.
|
|
||||||
<a
|
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
Learn more{" "}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||||
options={dataPlaneRBACOptionsList}
|
options={dataPlaneRBACOptionsList}
|
||||||
@@ -558,339 +538,316 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
selectedKey={enableDataPlaneRBACOption}
|
selectedKey={enableDataPlaneRBACOption}
|
||||||
onChange={handleOnDataPlaneRBACOptionChange}
|
onChange={handleOnDataPlaneRBACOptionChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</fieldset>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{userContext.apiType === "SQL" && (
|
|
||||||
<>
|
|
||||||
<AccordionItem value="3">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>Query Timeout</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
When a query reaches a specified time limit, a popup with an option to cancel the query will show
|
|
||||||
unless automatic cancellation has been enabled.
|
|
||||||
</div>
|
|
||||||
<Toggle
|
|
||||||
styles={toggleStyles}
|
|
||||||
label="Enable query timeout"
|
|
||||||
onChange={handleOnQueryTimeoutToggleChange}
|
|
||||||
defaultChecked={queryTimeoutEnabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{queryTimeoutEnabled && (
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<SpinButton
|
|
||||||
label="Query timeout (ms)"
|
|
||||||
labelPosition={Position.top}
|
|
||||||
defaultValue={(queryTimeout || 5000).toString()}
|
|
||||||
min={100}
|
|
||||||
step={1000}
|
|
||||||
onChange={handleOnQueryTimeoutSpinButtonChange}
|
|
||||||
incrementButtonAriaLabel="Increase value by 1000"
|
|
||||||
decrementButtonAriaLabel="Decrease value by 1000"
|
|
||||||
styles={spinButtonStyles}
|
|
||||||
/>
|
|
||||||
<Toggle
|
|
||||||
label="Automatically cancel query after timeout"
|
|
||||||
styles={toggleStyles}
|
|
||||||
onChange={handleOnAutomaticallyCancelQueryToggleChange}
|
|
||||||
defaultChecked={automaticallyCancelQueryAfterTimeout}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="4">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>RU Limit</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
If a query exceeds a configured RU limit, the query will be aborted.
|
|
||||||
</div>
|
|
||||||
<Toggle
|
|
||||||
styles={toggleStyles}
|
|
||||||
label="Enable RU limit"
|
|
||||||
onChange={handleOnRUThresholdToggleChange}
|
|
||||||
defaultChecked={ruThresholdEnabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{ruThresholdEnabled && (
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<SpinButton
|
|
||||||
label="RU Limit (RU)"
|
|
||||||
labelPosition={Position.top}
|
|
||||||
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
|
|
||||||
min={1}
|
|
||||||
step={1000}
|
|
||||||
onChange={handleOnRUThresholdSpinButtonChange}
|
|
||||||
incrementButtonAriaLabel="Increase value by 1000"
|
|
||||||
decrementButtonAriaLabel="Decrease value by 1000"
|
|
||||||
styles={spinButtonStyles}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="5">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>Default Query Results View</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Select the default view to use when displaying query results.
|
|
||||||
</div>
|
|
||||||
<ChoiceGroup
|
|
||||||
ariaLabelledBy="defaultQueryResultsView"
|
|
||||||
selectedKey={defaultQueryResultsView}
|
|
||||||
options={defaultQueryResultsViewOptionList}
|
|
||||||
styles={choiceButtonStyles}
|
|
||||||
onChange={handleOnDefaultQueryResultsViewChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{userContext.apiType === "SQL" && (
|
||||||
<AccordionItem value="6">
|
<>
|
||||||
<AccordionHeader>
|
<div className="settingsSection">
|
||||||
<div className={styles.header}>Retry Settings</div>
|
<div className="settingsSectionPart">
|
||||||
</AccordionHeader>
|
<div>
|
||||||
<AccordionPanel>
|
<legend id="ruThresholdLabel" className="settingsSectionLabel legendLabel">
|
||||||
<div className={styles.settingsSectionContainer}>
|
RU Threshold
|
||||||
<div className={styles.settingsSectionDescription}>
|
</legend>
|
||||||
Retry policy associated with throttled requests during CosmosDB queries.
|
<InfoTooltip>If a query exceeds a configured RU threshold, the query will be aborted.</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className={styles.subHeader}>Max retry attempts</span>
|
<Toggle
|
||||||
<InfoTooltip className={styles.headerIcon}>
|
styles={toggleStyles}
|
||||||
Max number of retries to be performed for a request. Default value 9.
|
label="Enable RU threshold"
|
||||||
</InfoTooltip>
|
onChange={handleOnRUThresholdToggleChange}
|
||||||
</div>
|
defaultChecked={ruThresholdEnabled}
|
||||||
<SpinButton
|
|
||||||
labelPosition={Position.top}
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
value={"" + retryAttempts}
|
|
||||||
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
|
|
||||||
incrementButtonAriaLabel="Increase value by 1"
|
|
||||||
decrementButtonAriaLabel="Decrease value by 1"
|
|
||||||
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
|
||||||
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
|
||||||
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
|
||||||
styles={spinButtonStyles}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
|
||||||
<InfoTooltip className={styles.headerIcon}>
|
|
||||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as
|
|
||||||
part of the response. Default value is 0 milliseconds.
|
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
|
||||||
<SpinButton
|
|
||||||
labelPosition={Position.top}
|
|
||||||
min={1000}
|
|
||||||
step={1000}
|
|
||||||
value={"" + retryInterval}
|
|
||||||
onChange={handleOnRetryIntervalSpinButtonChange}
|
|
||||||
incrementButtonAriaLabel="Increase value by 1000"
|
|
||||||
decrementButtonAriaLabel="Decrease value by 1000"
|
|
||||||
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
|
||||||
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
|
||||||
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
|
||||||
styles={spinButtonStyles}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<span className={styles.subHeader}>Max wait time (s)</span>
|
|
||||||
<InfoTooltip className={styles.headerIcon}>
|
|
||||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
|
||||||
seconds.
|
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
|
||||||
<SpinButton
|
|
||||||
labelPosition={Position.top}
|
|
||||||
min={1}
|
|
||||||
step={1}
|
|
||||||
value={"" + MaxWaitTimeInSeconds}
|
|
||||||
onChange={handleOnMaxWaitTimeSpinButtonChange}
|
|
||||||
incrementButtonAriaLabel="Increase value by 1"
|
|
||||||
decrementButtonAriaLabel="Decrease value by 1"
|
|
||||||
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
|
||||||
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
|
||||||
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
|
||||||
styles={spinButtonStyles}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="7">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>Enable container pagination</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
|
||||||
</div>
|
|
||||||
<Checkbox
|
|
||||||
styles={{
|
|
||||||
label: { padding: 0 },
|
|
||||||
}}
|
|
||||||
className="padding"
|
|
||||||
ariaLabel="Enable container pagination"
|
|
||||||
checked={containerPaginationEnabled}
|
|
||||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
|
||||||
label="Enable container pagination"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
{shouldShowCrossPartitionOption && (
|
|
||||||
<AccordionItem value="8">
|
|
||||||
<AccordionHeader>
|
|
||||||
<div className={styles.header}>Enable cross-partition query</div>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionPanel>
|
|
||||||
<div className={styles.settingsSectionContainer}>
|
|
||||||
<div className={styles.settingsSectionDescription}>
|
|
||||||
Send more than one request while executing a query. More than one request is necessary if the query
|
|
||||||
is not scoped to single partition key value.
|
|
||||||
</div>
|
|
||||||
<Checkbox
|
|
||||||
styles={{
|
|
||||||
label: { padding: 0 },
|
|
||||||
}}
|
|
||||||
className="padding"
|
|
||||||
ariaLabel="Enable cross partition query"
|
|
||||||
checked={crossPartitionQueryEnabled}
|
|
||||||
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
|
|
||||||
label="Enable cross-partition query"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
{ruThresholdEnabled && (
|
||||||
</AccordionItem>
|
<div>
|
||||||
)}
|
<SpinButton
|
||||||
|
label="RU Threshold (RU)"
|
||||||
{shouldShowParallelismOption && (
|
labelPosition={Position.top}
|
||||||
<AccordionItem value="9">
|
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
|
||||||
<AccordionHeader>
|
min={1}
|
||||||
<div className={styles.header}>Max degree of parallelism</div>
|
step={1000}
|
||||||
</AccordionHeader>
|
onChange={handleOnRUThresholdSpinButtonChange}
|
||||||
<AccordionPanel>
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
<div className={styles.settingsSectionContainer}>
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
<div className={styles.settingsSectionDescription}>
|
styles={spinButtonStyles}
|
||||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A
|
/>
|
||||||
positive property value limits the number of concurrent operations to the set value. If it is set to
|
|
||||||
less than 0, the system automatically decides the number of concurrent operations to run.
|
|
||||||
</div>
|
</div>
|
||||||
<SpinButton
|
)}
|
||||||
min={-1}
|
</div>
|
||||||
step={1}
|
</div>
|
||||||
className="textfontclr"
|
<div className="settingsSection">
|
||||||
role="textbox"
|
<div className="settingsSectionPart">
|
||||||
id="max-degree"
|
<div>
|
||||||
value={"" + maxDegreeOfParallelism}
|
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
|
||||||
onIncrement={(newValue) =>
|
Query Timeout
|
||||||
setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)
|
</legend>
|
||||||
}
|
<InfoTooltip>
|
||||||
onDecrement={(newValue) =>
|
When a query reaches a specified time limit, a popup with an option to cancel the query will show
|
||||||
setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)
|
unless automatic cancellation has been enabled
|
||||||
}
|
</InfoTooltip>
|
||||||
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
|
</div>
|
||||||
ariaLabel="Max degree of parallelism"
|
<div>
|
||||||
label="Max degree of parallelism"
|
<Toggle
|
||||||
|
styles={toggleStyles}
|
||||||
|
label="Enable query timeout"
|
||||||
|
onChange={handleOnQueryTimeoutToggleChange}
|
||||||
|
defaultChecked={queryTimeoutEnabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
{queryTimeoutEnabled && (
|
||||||
</AccordionItem>
|
<div>
|
||||||
)}
|
<SpinButton
|
||||||
|
label="Query timeout (ms)"
|
||||||
{shouldShowPriorityLevelOption && (
|
labelPosition={Position.top}
|
||||||
<AccordionItem value="10">
|
defaultValue={(queryTimeout || 5000).toString()}
|
||||||
<AccordionHeader>
|
min={100}
|
||||||
<div className={styles.header}>Priority Level</div>
|
step={1000}
|
||||||
</AccordionHeader>
|
onChange={handleOnQueryTimeoutSpinButtonChange}
|
||||||
<AccordionPanel>
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
<div className={styles.settingsSectionContainer}>
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
<div className={styles.settingsSectionDescription}>
|
styles={spinButtonStyles}
|
||||||
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
|
/>
|
||||||
Execution. If "None" is selected, Data Explorer will not specify priority level, and the
|
<Toggle
|
||||||
server-side default priority level will be used.
|
label="Automatically cancel query after timeout"
|
||||||
|
styles={toggleStyles}
|
||||||
|
onChange={handleOnAutomaticallyCancelQueryToggleChange}
|
||||||
|
defaultChecked={automaticallyCancelQueryAfterTimeout}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart">
|
||||||
|
<div>
|
||||||
|
<legend id="defaultQueryResultsView" className="settingsSectionLabel legendLabel">
|
||||||
|
Default Query Results View
|
||||||
|
</legend>
|
||||||
|
<InfoTooltip>Select the default view to use when displaying query results.</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
ariaLabelledBy="priorityLevel"
|
ariaLabelledBy="defaultQueryResultsView"
|
||||||
selectedKey={priorityLevel}
|
selectedKey={defaultQueryResultsView}
|
||||||
options={priorityLevelOptionList}
|
options={defaultQueryResultsViewOptionList}
|
||||||
styles={choiceButtonStyles}
|
styles={choiceButtonStyles}
|
||||||
onChange={handleOnPriorityLevelOptionChange}
|
onChange={handleOnDefaultQueryResultsViewChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionPanel>
|
</div>
|
||||||
</AccordionItem>
|
</div>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart">
|
||||||
|
<div className="settingsSectionLabel">
|
||||||
|
Retry Settings
|
||||||
|
<InfoTooltip>Retry policy associated with throttled requests during CosmosDB queries.</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
||||||
|
Max retry attempts
|
||||||
|
</legend>
|
||||||
|
<InfoTooltip>Max number of retries to be performed for a request. Default value 9.</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<SpinButton
|
||||||
|
labelPosition={Position.top}
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
value={"" + retryAttempts}
|
||||||
|
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
|
||||||
|
incrementButtonAriaLabel="Increase value by 1"
|
||||||
|
decrementButtonAriaLabel="Decrease value by 1"
|
||||||
|
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
||||||
|
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
||||||
|
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
||||||
|
styles={spinButtonStyles}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
||||||
|
Fixed retry interval (ms)
|
||||||
|
</legend>
|
||||||
|
<InfoTooltip>
|
||||||
|
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part
|
||||||
|
of the response. Default value is 0 milliseconds.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<SpinButton
|
||||||
|
labelPosition={Position.top}
|
||||||
|
min={1000}
|
||||||
|
step={1000}
|
||||||
|
value={"" + retryInterval}
|
||||||
|
onChange={handleOnRetryIntervalSpinButtonChange}
|
||||||
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
|
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
||||||
|
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
||||||
|
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
||||||
|
styles={spinButtonStyles}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
||||||
|
Max wait time (s)
|
||||||
|
</legend>
|
||||||
|
<InfoTooltip>
|
||||||
|
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||||
|
seconds.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<SpinButton
|
||||||
|
labelPosition={Position.top}
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
value={"" + MaxWaitTimeInSeconds}
|
||||||
|
onChange={handleOnMaxWaitTimeSpinButtonChange}
|
||||||
|
incrementButtonAriaLabel="Increase value by 1"
|
||||||
|
decrementButtonAriaLabel="Decrease value by 1"
|
||||||
|
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
||||||
|
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
||||||
|
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
||||||
|
styles={spinButtonStyles}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart settingsSectionInlineCheckbox">
|
||||||
|
<div className="settingsSectionLabel">
|
||||||
|
Enable container pagination
|
||||||
|
<InfoTooltip>
|
||||||
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
styles={{
|
||||||
|
label: { padding: 0 },
|
||||||
|
}}
|
||||||
|
className="padding"
|
||||||
|
ariaLabel="Enable container pagination"
|
||||||
|
checked={containerPaginationEnabled}
|
||||||
|
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{shouldShowCrossPartitionOption && (
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart settingsSectionInlineCheckbox">
|
||||||
|
<div className="settingsSectionLabel">
|
||||||
|
Enable cross-partition query
|
||||||
|
<InfoTooltip>
|
||||||
|
Send more than one request while executing a query. More than one request is necessary if the query is
|
||||||
|
not scoped to single partition key value.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
{shouldShowGraphAutoVizOption && (
|
<Checkbox
|
||||||
<AccordionItem value="11">
|
styles={{
|
||||||
<AccordionHeader>
|
label: { padding: 0 },
|
||||||
<div className={styles.header}>Display Gremlin query results as: </div>
|
}}
|
||||||
</AccordionHeader>
|
className="padding"
|
||||||
<AccordionPanel>
|
ariaLabel="Enable cross partition query"
|
||||||
<div className={styles.settingsSectionContainer}>
|
checked={crossPartitionQueryEnabled}
|
||||||
<div className={styles.settingsSectionDescription}>
|
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
|
||||||
Select Graph to automatically visualize the query results as a Graph or JSON to display the results
|
/>
|
||||||
as JSON.
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChoiceGroup
|
)}
|
||||||
selectedKey={graphAutoVizDisabled}
|
{shouldShowParallelismOption && (
|
||||||
options={graphAutoOptionList}
|
<div className="settingsSection">
|
||||||
onChange={handleOnGremlinChange}
|
<div className="settingsSectionPart">
|
||||||
aria-label="Graph Auto-visualization"
|
<div className="settingsSectionLabel">
|
||||||
/>
|
Max degree of parallelism
|
||||||
</div>
|
<InfoTooltip>
|
||||||
</AccordionPanel>
|
Gets or sets the number of concurrent operations run client side during parallel query execution. A
|
||||||
</AccordionItem>
|
positive property value limits the number of concurrent operations to the set value. If it is set to
|
||||||
)}
|
less than 0, the system automatically decides the number of concurrent operations to run.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
{shouldShowCopilotSampleDBOption && (
|
<SpinButton
|
||||||
<AccordionItem value="12">
|
min={-1}
|
||||||
<AccordionHeader>
|
step={1}
|
||||||
<div className={styles.header}>Enable sample database</div>
|
className="textfontclr"
|
||||||
</AccordionHeader>
|
role="textbox"
|
||||||
<AccordionPanel>
|
id="max-degree"
|
||||||
<div className={styles.settingsSectionContainer}>
|
value={"" + maxDegreeOfParallelism}
|
||||||
<div className={styles.settingsSectionDescription}>
|
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
|
||||||
This is a sample database and collection with synthetic product data you can use to explore using
|
onDecrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)}
|
||||||
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and
|
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
|
||||||
is created by, and maintained by Microsoft at no cost to you.
|
ariaLabel="Max degree of parallelism"
|
||||||
</div>
|
/>
|
||||||
<Checkbox
|
</div>
|
||||||
styles={{
|
</div>
|
||||||
label: { padding: 0 },
|
)}
|
||||||
}}
|
{shouldShowPriorityLevelOption && (
|
||||||
className="padding"
|
<div className="settingsSection">
|
||||||
ariaLabel="Enable sample db for Query Advisor"
|
<div className="settingsSectionPart">
|
||||||
checked={copilotSampleDBEnabled}
|
<fieldset>
|
||||||
onChange={handleSampleDatabaseChange}
|
<legend id="priorityLevel" className="settingsSectionLabel legendLabel">
|
||||||
label="Enable sample database"
|
Priority Level
|
||||||
/>
|
</legend>
|
||||||
</div>
|
<InfoTooltip>
|
||||||
</AccordionPanel>
|
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
|
||||||
</AccordionItem>
|
Execution. If "None" is selected, Data Explorer will not specify priority level, and the
|
||||||
)}
|
server-side default priority level will be used.
|
||||||
</Accordion>
|
</InfoTooltip>
|
||||||
|
<ChoiceGroup
|
||||||
|
ariaLabelledBy="priorityLevel"
|
||||||
|
selectedKey={priorityLevel}
|
||||||
|
options={priorityLevelOptionList}
|
||||||
|
styles={choiceButtonStyles}
|
||||||
|
onChange={handleOnPriorityLevelOptionChange}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{shouldShowGraphAutoVizOption && (
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart">
|
||||||
|
<div className="settingsSectionLabel">
|
||||||
|
Display Gremlin query results as:
|
||||||
|
<InfoTooltip>
|
||||||
|
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as
|
||||||
|
JSON.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChoiceGroup
|
||||||
|
selectedKey={graphAutoVizDisabled}
|
||||||
|
options={graphAutoOptionList}
|
||||||
|
onChange={handleOnGremlinChange}
|
||||||
|
aria-label="Graph Auto-visualization"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{shouldShowCopilotSampleDBOption && (
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart settingsSectionInlineCheckbox">
|
||||||
|
<div className="settingsSectionLabel">
|
||||||
|
Enable sample database
|
||||||
|
<InfoTooltip>
|
||||||
|
This is a sample database and collection with synthetic product data you can use to explore using
|
||||||
|
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is
|
||||||
|
created by, and maintained by Microsoft at no cost to you.
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
styles={{
|
||||||
|
label: { padding: 0 },
|
||||||
|
}}
|
||||||
|
className="padding"
|
||||||
|
ariaLabel="Enable sample db for Query Advisor"
|
||||||
|
checked={copilotSampleDBEnabled}
|
||||||
|
onChange={handleSampleDatabaseChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart">
|
<div className="settingsSectionPart">
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,156 +0,0 @@
|
|||||||
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 {
|
|
||||||
/* selectedColumnIds may contain ids that are not in columnDefinitions, because the selected
|
|
||||||
* ids may have been loaded from persistence, but don't exist in the current retrieved documents.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (
|
|
||||||
Array.from(selectedColumnIdsSet).filter((id) => columnDefinitions.find((def) => def.id === id) !== undefined)
|
|
||||||
.length === 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -171,7 +171,7 @@ export const QueryCopilotCarousel: React.FC<QueryCopilotCarouselProps> = ({
|
|||||||
the query builder.
|
the query builder.
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
|
||||||
<Text style={{ fontSize: 13 }}>CopilotSampleDB</Text>
|
<Text style={{ fontSize: 13 }}>CopilotSampleDb</Text>
|
||||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
|
||||||
<Text style={{ fontSize: 13 }}>Autoscale</Text>
|
<Text style={{ fontSize: 13 }}>Autoscale</Text>
|
||||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
|
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import {
|
|||||||
SuggestedPrompt,
|
SuggestedPrompt,
|
||||||
getSampleDatabaseSuggestedPrompts,
|
getSampleDatabaseSuggestedPrompts,
|
||||||
getSuggestedPrompts,
|
getSuggestedPrompts,
|
||||||
readPromptHistory,
|
|
||||||
savePromptHistory,
|
|
||||||
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
@@ -138,7 +136,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||||
const [histories, setHistories] = useState<string[]>(() => readPromptHistory(userContext.databaseAccount));
|
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
||||||
|
const cachedHistories = cachedHistoriesString?.split("|");
|
||||||
|
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
||||||
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
|
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
|
||||||
? getSampleDatabaseSuggestedPrompts()
|
? getSampleDatabaseSuggestedPrompts()
|
||||||
: getSuggestedPrompts();
|
: getSuggestedPrompts();
|
||||||
@@ -172,7 +172,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||||
|
|
||||||
setHistories(newHistories);
|
setHistories(newHistories);
|
||||||
savePromptHistory(userContext.databaseAccount, newHistories);
|
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetMessageStates = (): void => {
|
const resetMessageStates = (): void => {
|
||||||
|
|||||||
@@ -1,39 +1,10 @@
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { CopilotSubComponentNames } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppStateComponentNames, StorePath } from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { updateUserContext } from "UserContext";
|
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { QueryCopilotTab } from "./QueryCopilotTab";
|
import { QueryCopilotTab } from "./QueryCopilotTab";
|
||||||
|
|
||||||
describe("Query copilot tab snapshot test", () => {
|
describe("Query copilot tab snapshot test", () => {
|
||||||
it("should render with initial input", () => {
|
it("should render with initial input", () => {
|
||||||
updateUserContext({
|
|
||||||
databaseAccount: {
|
|
||||||
name: "name",
|
|
||||||
properties: undefined,
|
|
||||||
id: "",
|
|
||||||
location: "",
|
|
||||||
type: "",
|
|
||||||
kind: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadState = (path: StorePath) => {
|
|
||||||
if (
|
|
||||||
path.componentName === AppStateComponentNames.QueryCopilot &&
|
|
||||||
path.subComponentName === CopilotSubComponentNames.toggleStatus
|
|
||||||
) {
|
|
||||||
return { enabled: true };
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
|
||||||
loadState,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { Stack } from "@fluentui/react";
|
|||||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
|
||||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||||
@@ -19,13 +18,18 @@ import SplitterLayout from "react-splitter-layout";
|
|||||||
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
||||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||||
|
import * as StringUtility from "../../Shared/StringUtility";
|
||||||
|
|
||||||
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
||||||
|
|
||||||
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
||||||
readCopilotToggleStatus(userContext.databaseAccount),
|
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
||||||
);
|
);
|
||||||
|
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
||||||
|
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
||||||
|
: true;
|
||||||
|
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
|
||||||
const [tabActive, setTabActive] = useState<boolean>(true);
|
const [tabActive, setTabActive] = useState<boolean>(true);
|
||||||
|
|
||||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
@@ -84,7 +88,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
|
|
||||||
const toggleCopilot = (toggle: boolean) => {
|
const toggleCopilot = (toggle: boolean) => {
|
||||||
setCopilotActive(toggle);
|
setCopilotActive(toggle);
|
||||||
saveCopilotToggleStatus(userContext.databaseAccount, toggle);
|
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, toggle.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ describe("QueryCopilotUtilities", () => {
|
|||||||
|
|
||||||
// Mock the items.query method to return the mockResult
|
// Mock the items.query method to return the mockResult
|
||||||
(
|
(
|
||||||
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").items.query as jest.Mock
|
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").items.query as jest.Mock
|
||||||
).mockReturnValue(mockResult);
|
).mockReturnValue(mockResult);
|
||||||
|
|
||||||
const result = querySampleDocuments(query, options);
|
const result = querySampleDocuments(query, options);
|
||||||
@@ -119,10 +119,10 @@ describe("QueryCopilotUtilities", () => {
|
|||||||
const result = await readSampleDocument(documentId);
|
const result = await readSampleDocument(documentId);
|
||||||
|
|
||||||
expect(sampleDataClient).toHaveBeenCalled();
|
expect(sampleDataClient).toHaveBeenCalled();
|
||||||
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDB");
|
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
|
||||||
expect(sampleDataClient().database("CopilotSampleDB").container).toHaveBeenCalledWith("SampleContainer");
|
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
|
||||||
expect(
|
expect(
|
||||||
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").item("DocumentId", undefined).read,
|
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read,
|
||||||
).toHaveBeenCalled();
|
).toHaveBeenCalled();
|
||||||
expect(result).toEqual(expectedResponse);
|
expect(result).toEqual(expectedResponse);
|
||||||
});
|
});
|
||||||
@@ -144,10 +144,10 @@ describe("QueryCopilotUtilities", () => {
|
|||||||
await expect(readSampleDocument(documentId)).rejects.toStrictEqual(errorMock);
|
await expect(readSampleDocument(documentId)).rejects.toStrictEqual(errorMock);
|
||||||
|
|
||||||
expect(sampleDataClient).toHaveBeenCalled();
|
expect(sampleDataClient).toHaveBeenCalled();
|
||||||
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDB");
|
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
|
||||||
expect(sampleDataClient().database("CopilotSampleDB").container).toHaveBeenCalledWith("SampleContainer");
|
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
|
||||||
expect(
|
expect(
|
||||||
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").item("DocumentId", undefined).read,
|
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read,
|
||||||
).toHaveBeenCalled();
|
).toHaveBeenCalled();
|
||||||
expect(handleError).toHaveBeenCalledWith(errorMock, "ReadDocument", expect.any(String));
|
expect(handleError).toHaveBeenCalledWith(errorMock, "ReadDocument", expect.any(String));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ import { handleError } from "Common/ErrorHandlingUtils";
|
|||||||
import { sampleDataClient } from "Common/SampleDataClient";
|
import { sampleDataClient } from "Common/SampleDataClient";
|
||||||
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
||||||
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
||||||
import { DatabaseAccount } from "Contracts/DataModels";
|
|
||||||
import DocumentId from "Explorer/Tree/DocumentId";
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
import { AppStateComponentNames, loadState, saveState } from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
import * as StringUtility from "../../Shared/StringUtility";
|
|
||||||
|
|
||||||
export interface SuggestedPrompt {
|
export interface SuggestedPrompt {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -57,110 +54,3 @@ export const getSuggestedPrompts = (): SuggestedPrompt[] => {
|
|||||||
{ id: 3, text: "Find the oldest item added to my collection" },
|
{ id: 3, text: "Find the oldest item added to my collection" },
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prompt history persistence
|
|
||||||
export enum CopilotSubComponentNames {
|
|
||||||
promptHistory = "PromptHistory",
|
|
||||||
toggleStatus = "ToggleStatus",
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLegacyHistoryKey = (databaseAccount: DatabaseAccount): string =>
|
|
||||||
`${databaseAccount?.id}-queryCopilotHistories`;
|
|
||||||
const getLegacyToggleStatusKey = (databaseAccount: DatabaseAccount): string =>
|
|
||||||
`${databaseAccount?.id}-queryCopilotToggleStatus`;
|
|
||||||
|
|
||||||
// Migration only needs to run once
|
|
||||||
let hasMigrated = false;
|
|
||||||
// Migrate old prompt history to new format
|
|
||||||
export const migrateCopilotPersistence = (databaseAccount: DatabaseAccount): void => {
|
|
||||||
if (hasMigrated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = getLegacyHistoryKey(databaseAccount);
|
|
||||||
let item = localStorage.getItem(key);
|
|
||||||
if (item !== undefined && item !== null) {
|
|
||||||
const historyItems = item.split("|");
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.promptHistory,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
},
|
|
||||||
historyItems,
|
|
||||||
);
|
|
||||||
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
key = getLegacyToggleStatusKey(databaseAccount);
|
|
||||||
item = localStorage.getItem(key);
|
|
||||||
if (item !== undefined && item !== null) {
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.toggleStatus,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
},
|
|
||||||
StringUtility.toBoolean(item),
|
|
||||||
);
|
|
||||||
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMigrated = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const readPromptHistory = (databaseAccount: DatabaseAccount): string[] => {
|
|
||||||
migrateCopilotPersistence(databaseAccount);
|
|
||||||
return (
|
|
||||||
(loadState({
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.promptHistory,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
}) as string[]) || []
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const savePromptHistory = (databaseAccount: DatabaseAccount, historyItems: string[]): void => {
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.promptHistory,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
},
|
|
||||||
historyItems,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const readCopilotToggleStatus = (databaseAccount: DatabaseAccount): boolean => {
|
|
||||||
migrateCopilotPersistence(databaseAccount);
|
|
||||||
return !!loadState({
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.toggleStatus,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
}) as boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveCopilotToggleStatus = (databaseAccount: DatabaseAccount, status: boolean): void => {
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.QueryCopilot,
|
|
||||||
subComponentName: CopilotSubComponentNames.toggleStatus,
|
|
||||||
globalAccountName: databaseAccount.name,
|
|
||||||
databaseName: undefined,
|
|
||||||
containerName: undefined,
|
|
||||||
},
|
|
||||||
status,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
|
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { querySampleDocuments, readCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -36,6 +36,7 @@ import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
|||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
import * as StringUtility from "../../../Shared/StringUtility";
|
||||||
|
|
||||||
async function fetchWithTimeout(
|
async function fetchWithTimeout(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -360,7 +361,9 @@ export const QueryDocumentsPerPage = async (
|
|||||||
correlationId: useQueryCopilot.getState().correlationId,
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isCopilotActive = readCopilotToggleStatus(userContext.databaseAccount);
|
const isCopilotActive = StringUtility.toBoolean(
|
||||||
|
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`),
|
||||||
|
);
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
correlationId: useQueryCopilot.getState().correlationId,
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
|
|||||||
@@ -17,6 +17,38 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<QueryCopilotPromptbar
|
||||||
|
containerId="SampleContainer"
|
||||||
|
databaseId="CopilotSampleDb"
|
||||||
|
explorer={
|
||||||
|
Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
|
"isTabsContentExpanded": [Function],
|
||||||
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {
|
||||||
|
"armResourceId": undefined,
|
||||||
|
"retryOptions": {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provideFeedbackEmail": [Function],
|
||||||
|
"queriesClient": QueriesClient {
|
||||||
|
"container": [Circular],
|
||||||
|
},
|
||||||
|
"refreshNotebookList": [Function],
|
||||||
|
"resourceTree": ResourceTreeAdapter {
|
||||||
|
"container": [Circular],
|
||||||
|
"copyNotebook": [Function],
|
||||||
|
"parameters": [Function],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toggleCopilot={[Function]}
|
||||||
|
/>
|
||||||
<Stack
|
<Stack
|
||||||
className="tabPaneContentContainer"
|
className="tabPaneContentContainer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -26,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, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
|
|
||||||
const useSidebarStyles = makeStyles({
|
const useSidebarStyles = makeStyles({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
@@ -86,7 +86,7 @@ const useSidebarStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
globalCommandsMenuButton: {
|
globalCommandsMenuButton: {
|
||||||
display: "inline-flex",
|
display: "initial",
|
||||||
"@container (min-width: 250px)": {
|
"@container (min-width: 250px)": {
|
||||||
display: "none",
|
display: "none",
|
||||||
},
|
},
|
||||||
@@ -113,12 +113,6 @@ 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 ||
|
||||||
@@ -188,10 +182,10 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
{primaryAction.label}
|
{primaryAction.label}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Menu positioning={{ target: globalCommandButton, position: "below", align: "end" }}>
|
<Menu positioning="below-end">
|
||||||
<MenuTrigger disableButtonEnhancement>
|
<MenuTrigger disableButtonEnhancement>
|
||||||
{(triggerProps: MenuButtonProps) => (
|
{(triggerProps: MenuButtonProps) => (
|
||||||
<div ref={setGlobalCommandButton}>
|
<>
|
||||||
<SplitButton
|
<SplitButton
|
||||||
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
||||||
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
||||||
@@ -203,7 +197,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
<MenuButton {...triggerProps} icon={primaryAction.icon} className={styles.globalCommandsMenuButton}>
|
<MenuButton {...triggerProps} icon={primaryAction.icon} className={styles.globalCommandsMenuButton}>
|
||||||
New...
|
New...
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
<MenuPopover>
|
<MenuPopover>
|
||||||
@@ -286,7 +280,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={250}>
|
<Allotment.Pane minSize={24} preferredSize={300}>
|
||||||
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
||||||
<div className={styles.sidebarContainer}>
|
<div className={styles.sidebarContainer}>
|
||||||
{loading && (
|
{loading && (
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private clearMostRecent = (): void => {
|
private clearMostRecent = (): void => {
|
||||||
MostRecentActivity.clear(userContext.databaseAccount?.name);
|
MostRecentActivity.mostRecentActivity.clear(userContext.databaseAccount?.id);
|
||||||
this.setState({});
|
this.setState({});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -498,7 +498,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createRecentItems(): SplashScreenItem[] {
|
private createRecentItems(): SplashScreenItem[] {
|
||||||
return MostRecentActivity.getItems(userContext.databaseAccount?.name).map((activity) => {
|
return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => {
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
default: {
|
default: {
|
||||||
const unknownActivity: never = activity;
|
const unknownActivity: never = activity;
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ export const htmlAttributeNames = {
|
|||||||
dataTableContentTypeAttr: "contentType_attr",
|
dataTableContentTypeAttr: "contentType_attr",
|
||||||
dataTableSnapshotAttr: "snapshot_attr",
|
dataTableSnapshotAttr: "snapshot_attr",
|
||||||
dataTableRowKeyAttr: "rowKey_attr",
|
dataTableRowKeyAttr: "rowKey_attr",
|
||||||
dataTablePartitionKeyAttr: "partKey_attr",
|
|
||||||
dataTableMessageIdAttr: "messageId_attr",
|
dataTableMessageIdAttr: "messageId_attr",
|
||||||
dataTableHeaderIndex: "data-column-index",
|
dataTableHeaderIndex: "data-column-index",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -193,9 +193,6 @@ function getServerData(sSource: any, aoData: any, fnCallback: any, oSettings: an
|
|||||||
* from UI elements.
|
* from UI elements.
|
||||||
*/
|
*/
|
||||||
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
|
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
|
||||||
if (aData.PartitionKey && aData.PartitionKey._) {
|
|
||||||
$(nRow).attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr, aData.PartitionKey._);
|
|
||||||
}
|
|
||||||
$(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
|
$(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
|
||||||
return nRow;
|
return nRow;
|
||||||
}
|
}
|
||||||
@@ -208,10 +205,6 @@ function selectionChanged(element: any, valueAccessor: any, allBindings: any, vi
|
|||||||
selected &&
|
selected &&
|
||||||
selected.forEach((b: Entities.ITableEntity) => {
|
selected.forEach((b: Entities.ITableEntity) => {
|
||||||
var sel = DataTableOperations.getRowSelector([
|
var sel = DataTableOperations.getRowSelector([
|
||||||
{
|
|
||||||
key: Constants.htmlAttributeNames.dataTablePartitionKeyAttr,
|
|
||||||
value: b.PartitionKey && b.PartitionKey._ && b.PartitionKey._.toString(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
|
key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
|
||||||
value: b.RowKey && b.RowKey._ && b.RowKey._.toString(),
|
value: b.RowKey && b.RowKey._ && b.RowKey._.toString(),
|
||||||
@@ -377,9 +370,8 @@ function updateSelectionStatus(oSettings: any): void {
|
|||||||
for (var i = 0; i < $dataTableRows.length; i++) {
|
for (var i = 0; i < $dataTableRows.length; i++) {
|
||||||
var $row: JQuery = $dataTableRows.eq(i);
|
var $row: JQuery = $dataTableRows.eq(i);
|
||||||
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
|
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
|
||||||
var partitionKey: string = $row.attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr);
|
|
||||||
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
|
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
|
||||||
if (table.isItemSelected(table.getTableEntityKeys(rowKey, partitionKey))) {
|
if (table.isItemSelected(table.getTableEntityKeys(rowKey))) {
|
||||||
$row.attr("tabindex", "0");
|
$row.attr("tabindex", "0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,10 +56,7 @@ export default class DataTableOperationManager {
|
|||||||
// Simply select the first item in this case.
|
// Simply select the first item in this case.
|
||||||
var lastSelectedItemIndex = lastSelectedItem
|
var lastSelectedItemIndex = lastSelectedItem
|
||||||
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
|
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(
|
this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._),
|
||||||
lastSelectedItem.RowKey._,
|
|
||||||
lastSelectedItem.PartitionKey && lastSelectedItem.PartitionKey._,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: -1;
|
: -1;
|
||||||
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
||||||
@@ -150,14 +147,13 @@ export default class DataTableOperationManager {
|
|||||||
private getEntityIdentity($elem: JQuery<Element>): Entities.ITableEntityIdentity {
|
private getEntityIdentity($elem: JQuery<Element>): Entities.ITableEntityIdentity {
|
||||||
return {
|
return {
|
||||||
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
|
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
|
||||||
PartitionKey: $elem.attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateLastSelectedItem($elem: JQuery<Element>, isShiftSelect: boolean) {
|
private updateLastSelectedItem($elem: JQuery<Element>, isShiftSelect: boolean) {
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||||
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
this._tableEntityListViewModel.lastSelectedItem = entity;
|
this._tableEntityListViewModel.lastSelectedItem = entity;
|
||||||
@@ -172,7 +168,7 @@ export default class DataTableOperationManager {
|
|||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||||
|
|
||||||
this._tableEntityListViewModel.clearSelection();
|
this._tableEntityListViewModel.clearSelection();
|
||||||
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
this.addToSelection(entityIdentity.RowKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,11 +190,11 @@ export default class DataTableOperationManager {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!this._tableEntityListViewModel.isItemSelected(
|
!this._tableEntityListViewModel.isItemSelected(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Adding item not previously in selection
|
// Adding item not previously in selection
|
||||||
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
this.addToSelection(entityIdentity.RowKey);
|
||||||
} else {
|
} else {
|
||||||
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
||||||
}
|
}
|
||||||
@@ -216,10 +212,10 @@ export default class DataTableOperationManager {
|
|||||||
if (anchorItem) {
|
if (anchorItem) {
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||||
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
||||||
);
|
);
|
||||||
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.PartitionKey._, anchorItem.RowKey._),
|
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._),
|
||||||
);
|
);
|
||||||
|
|
||||||
var startIndex = Math.min(elementIndex, anchorIndex);
|
var startIndex = Math.min(elementIndex, anchorIndex);
|
||||||
@@ -238,25 +234,24 @@ export default class DataTableOperationManager {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!this._tableEntityListViewModel.isItemSelected(
|
!this._tableEntityListViewModel.isItemSelected(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (this._tableEntityListViewModel.selected().length) {
|
if (this._tableEntityListViewModel.selected().length) {
|
||||||
this._tableEntityListViewModel.clearSelection();
|
this._tableEntityListViewModel.clearSelection();
|
||||||
}
|
}
|
||||||
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
this.addToSelection(entityIdentity.RowKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addToSelection(rowKey: string, partitionKey?: string) {
|
private addToSelection(rowKey: string) {
|
||||||
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(rowKey, partitionKey),
|
this._tableEntityListViewModel.getTableEntityKeys(rowKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedEntity != null) {
|
if (selectedEntity != null) {
|
||||||
this._tableEntityListViewModel.selected.push(selectedEntity);
|
this._tableEntityListViewModel.selected.push(selectedEntity);
|
||||||
}
|
}
|
||||||
console.log(this._tableEntityListViewModel.selected().length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selecting first row if the selection is empty.
|
// Selecting first row if the selection is empty.
|
||||||
@@ -274,7 +269,7 @@ export default class DataTableOperationManager {
|
|||||||
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
|
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
|
||||||
this._tableEntityListViewModel.clearLastSelected();
|
this._tableEntityListViewModel.clearLastSelected();
|
||||||
|
|
||||||
this.addToSelection(firstEntity.RowKey._, firstEntity.PartitionKey && firstEntity.PartitionKey._);
|
this.addToSelection(firstEntity.RowKey._);
|
||||||
|
|
||||||
// Update last selection
|
// Update last selection
|
||||||
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
||||||
|
|||||||
@@ -128,14 +128,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
this.sqlQuery = ko.observable<string>("SELECT * FROM c");
|
this.sqlQuery = ko.observable<string>("SELECT * FROM c");
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTableEntityKeys(rowKey: string, partitionKey: string): Entities.IProperty[] {
|
public getTableEntityKeys(rowKey: string): Entities.IProperty[] {
|
||||||
const properties: Entities.IProperty[] = [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
|
return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
|
||||||
|
|
||||||
if (partitionKey) {
|
|
||||||
properties.push({ key: Constants.EntityKeyNames.PartitionKey, value: partitionKey });
|
|
||||||
}
|
|
||||||
|
|
||||||
return properties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.Api<Element> {
|
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.Api<Element> {
|
||||||
@@ -267,8 +261,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
}
|
}
|
||||||
var oldEntityIndex: number = _.findIndex(
|
var oldEntityIndex: number = _.findIndex(
|
||||||
this.cache.data,
|
this.cache.data,
|
||||||
(data: Entities.ITableEntity) =>
|
(data: Entities.ITableEntity) => data.RowKey._ === entity.RowKey._,
|
||||||
data.RowKey._ === entity.RowKey._ && data.PartitionKey._ === entity.PartitionKey._,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cache.data.splice(oldEntityIndex, 1, entity);
|
this.cache.data.splice(oldEntityIndex, 1, entity);
|
||||||
@@ -292,7 +285,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
entities.forEach((entity: Entities.ITableEntity) => {
|
entities.forEach((entity: Entities.ITableEntity) => {
|
||||||
var cachedIndex: number = _.findIndex(
|
var cachedIndex: number = _.findIndex(
|
||||||
this.cache.data,
|
this.cache.data,
|
||||||
(e: Entities.ITableEntity) => e.RowKey._ === entity.RowKey._ && e.PartitionKey._ === entity.PartitionKey._,
|
(e: Entities.ITableEntity) => e.RowKey._ === entity.RowKey._,
|
||||||
);
|
);
|
||||||
if (cachedIndex >= 0) {
|
if (cachedIndex >= 0) {
|
||||||
this.cache.data.splice(cachedIndex, 1);
|
this.cache.data.splice(cachedIndex, 1);
|
||||||
@@ -400,16 +393,6 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override as Tables can have the same Row key in different Partition keys
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
public getItemFromCurrentPage(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
|
|
||||||
return _.find(this.items(), (item: Entities.ITableEntity) => {
|
|
||||||
return this.matchesKeys(item, itemKeys);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private prefetchAndRender(
|
private prefetchAndRender(
|
||||||
tableQuery: Entities.ITableQuery,
|
tableQuery: Entities.ITableQuery,
|
||||||
tablePageStartIndex: number,
|
tablePageStartIndex: number,
|
||||||
|
|||||||
@@ -36,5 +36,4 @@ export interface ITableQuery {
|
|||||||
|
|
||||||
export interface ITableEntityIdentity {
|
export interface ITableEntityIdentity {
|
||||||
RowKey: string;
|
RowKey: string;
|
||||||
PartitionKey?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -753,11 +753,17 @@ 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)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,34 +1,21 @@
|
|||||||
// Definitions of State data
|
// Definitions of State data
|
||||||
|
|
||||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
import { deleteState, loadState, saveState, saveStateDebounced } from "Shared/AppStatePersistenceUtility";
|
||||||
import {
|
|
||||||
AppStateComponentNames,
|
|
||||||
deleteState,
|
|
||||||
loadState,
|
|
||||||
saveState,
|
|
||||||
saveStateDebounced,
|
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
const componentName = AppStateComponentNames.DocumentsTab;
|
const componentName = "DocumentsTab";
|
||||||
|
|
||||||
export enum SubComponentName {
|
export enum SubComponentName {
|
||||||
ColumnSizes = "ColumnSizes",
|
ColumnSizes = "ColumnSizes",
|
||||||
FilterHistory = "FilterHistory",
|
FilterHistory = "FilterHistory",
|
||||||
MainTabDivider = "MainTabDivider",
|
MainTabDivider = "MainTabDivider",
|
||||||
ColumnsSelection = "ColumnsSelection",
|
|
||||||
ColumnSort = "ColumnSort",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
export type ColumnSizesMap = { [columnId: string]: WidthDefinition };
|
||||||
export type FilterHistory = string[];
|
|
||||||
export type WidthDefinition = { widthPx: number };
|
export type WidthDefinition = { widthPx: number };
|
||||||
export type TabDivider = { leftPaneWidthPercent: number };
|
export type TabDivider = { leftPaneWidthPercent: number };
|
||||||
export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] };
|
|
||||||
export type ColumnSort = { columnId: string; direction: "ascending" | "descending" };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
|
import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
|
||||||
import { waitFor } from "@testing-library/react";
|
|
||||||
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
||||||
import { Platform, updateConfigContext } from "ConfigContext";
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
|
||||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import {
|
import {
|
||||||
ButtonsDependencies,
|
ButtonsDependencies,
|
||||||
DELETE_BUTTON_ID,
|
DELETE_BUTTON_ID,
|
||||||
@@ -68,14 +65,12 @@ jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
|||||||
EditorReact: (props: EditorReactProps) => <>{props.content}</>,
|
EditorReact: (props: EditorReactProps) => <>{props.content}</>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockDialogState = {
|
|
||||||
showOkCancelModalDialog: jest.fn((title: string, subText: string, okLabel: string, onOk: () => void) => onOk()),
|
|
||||||
showOkModalDialog: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Dialog", () => ({
|
jest.mock("Explorer/Controls/Dialog", () => ({
|
||||||
useDialog: {
|
useDialog: {
|
||||||
getState: jest.fn(() => mockDialogState),
|
getState: jest.fn(() => ({
|
||||||
|
showOkCancelModalDialog: (title: string, subText: string, okLabel: string, onOk: () => void) => onOk(),
|
||||||
|
showOkModalDialog: () => {},
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -85,10 +80,6 @@ jest.mock("Common/dataAccess/deleteDocument", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/ProgressModalDialog", () => ({
|
|
||||||
ProgressModalDialog: jest.fn(() => <></>),
|
|
||||||
}));
|
|
||||||
|
|
||||||
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
|
async function waitForComponentToPaint<P = unknown>(wrapper: ReactWrapper<P> | ShallowWrapper<P>, amount = 0) {
|
||||||
let newWrapper;
|
let newWrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -101,13 +92,7 @@ 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(
|
expect(buildQuery(false, "")).toContain("select");
|
||||||
buildQuery(false, "", ["pk"], {
|
|
||||||
paths: ["pk"],
|
|
||||||
kind: "Hash",
|
|
||||||
version: 2,
|
|
||||||
}),
|
|
||||||
).toContain("select");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -461,7 +446,7 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -471,36 +456,14 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking Delete Document asks for confirmation", async () => {
|
it("clicking Delete Document asks for confirmation", () => {
|
||||||
act(async () => {
|
|
||||||
await useCommandBar
|
|
||||||
.getState()
|
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
|
||||||
.onCommandClick(undefined, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(useDialog.getState().showOkCancelModalDialog).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clicking Delete Document for NoSql shows progress dialog", () => {
|
|
||||||
act(() => {
|
|
||||||
useCommandBar
|
|
||||||
.getState()
|
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
|
||||||
.onCommandClick(undefined, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ProgressModalDialog).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clicking Delete Document eventually calls delete client api", () => {
|
|
||||||
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
||||||
mockDeleteDocuments.mockClear();
|
mockDeleteDocuments.mockClear();
|
||||||
|
|
||||||
@@ -508,11 +471,10 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The implementation uses setTimeout, so wait for it to finish
|
expect(mockDeleteDocuments).toHaveBeenCalled();
|
||||||
waitFor(() => expect(mockDeleteDocuments).toHaveBeenCalled());
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import {
|
import { Button, Input, TableRowId, makeStyles, shorthands } from "@fluentui/react-components";
|
||||||
Button,
|
import { ArrowClockwise16Filled, Dismiss16Filled } from "@fluentui/react-icons";
|
||||||
Input,
|
|
||||||
Link,
|
|
||||||
MessageBar,
|
|
||||||
MessageBarBody,
|
|
||||||
MessageBarTitle,
|
|
||||||
TableRowId,
|
|
||||||
makeStyles,
|
|
||||||
shorthands,
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
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,
|
||||||
@@ -26,19 +17,15 @@ import { Platform, configContext } from "ConfigContext";
|
|||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import {
|
import {
|
||||||
ColumnsSelection,
|
|
||||||
FilterHistory,
|
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
TabDivider,
|
TabDivider,
|
||||||
readSubComponentState,
|
readSubComponentState,
|
||||||
saveSubComponentState,
|
saveSubComponentState,
|
||||||
} from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil";
|
} 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";
|
||||||
@@ -46,7 +33,7 @@ import { QueryConstants } from "Shared/Constants";
|
|||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { format } from "react-string-format";
|
import { format } from "react-string-format";
|
||||||
@@ -64,16 +51,13 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import { CollectionBase } 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 { defaultQueryFields, extractPartitionKeyValues } from "../../../Utils/QueryUtils";
|
import { 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 { ColumnDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
import { 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 MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we can afford to keep more items than fit on the screen
|
||||||
const NO_SQL_THROTTLING_DOC_URL =
|
|
||||||
"https://learn.microsoft.com/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large";
|
|
||||||
const MONGO_THROTTLING_DOC_URL = "https://learn.microsoft.com/azure/cosmos-db/mongodb/prevent-rate-limiting-errors";
|
|
||||||
|
|
||||||
const loadMoreHeight = LayoutConstants.rowHeight;
|
const loadMoreHeight = LayoutConstants.rowHeight;
|
||||||
export const useDocumentsTabStyles = makeStyles({
|
export const useDocumentsTabStyles = makeStyles({
|
||||||
@@ -105,13 +89,6 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
tableCell: {
|
tableCell: {
|
||||||
...cosmosShorthands.borderLeft(),
|
...cosmosShorthands.borderLeft(),
|
||||||
},
|
},
|
||||||
tableHeader: {
|
|
||||||
display: "flex",
|
|
||||||
},
|
|
||||||
tableHeaderFiller: {
|
|
||||||
width: "20px",
|
|
||||||
boxShadow: `0px -1px ${tokens.colorNeutralStroke2} inset`,
|
|
||||||
},
|
|
||||||
loadMore: {
|
loadMore: {
|
||||||
...cosmosShorthands.borderTop(),
|
...cosmosShorthands.borderTop(),
|
||||||
display: "grid",
|
display: "grid",
|
||||||
@@ -135,9 +112,6 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
deleteProgressContent: {
|
|
||||||
paddingTop: tokens.spacingVerticalL,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export class DocumentsTabV2 extends TabsBase {
|
export class DocumentsTabV2 extends TabsBase {
|
||||||
@@ -307,7 +281,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.openUploadItemsPane();
|
selectedCollection && container.openUploadItemsPanePane();
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
@@ -495,33 +469,17 @@ 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 || "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out fields starting with "/" (partition keys)
|
return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey);
|
||||||
return QueryUtils.buildDocumentsQuery(
|
|
||||||
filter,
|
|
||||||
partitionKeyProperties,
|
|
||||||
partitionKey,
|
|
||||||
additionalField?.filter((f) => !f.startsWith("/")) || [],
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -558,18 +516,9 @@ export interface IDocumentsTabComponentProps {
|
|||||||
|
|
||||||
const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`;
|
const getUniqueId = (collection: ViewModels.CollectionBase): string => `${collection.databaseId}-${collection.id()}`;
|
||||||
|
|
||||||
const getDefaultSqlFilters = (partitionKeys: string[]) =>
|
const defaultSqlFilters = ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC'];
|
||||||
['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 } }"];
|
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 - 27;
|
|
||||||
|
|
||||||
// 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,
|
||||||
@@ -588,7 +537,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<ExtendedDocumentId[]>([]);
|
const [documentIds, setDocumentIds] = useState<DocumentId[]>([]);
|
||||||
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();
|
||||||
@@ -619,7 +568,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
const [tabStateData, setTabStateData] = useState<TabDivider>(() =>
|
||||||
readSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, {
|
readSubComponentState(SubComponentName.MainTabDivider, _collection, {
|
||||||
leftPaneWidthPercent: 35,
|
leftPaneWidthPercent: 35,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -633,28 +582,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
const [continuationToken, setContinuationToken] = useState<string>(undefined);
|
const [continuationToken, setContinuationToken] = useState<string>(undefined);
|
||||||
|
|
||||||
// User's filter history
|
// User's filter history
|
||||||
const [lastFilterContents, setLastFilterContents] = useState<FilterHistory>(() =>
|
const [lastFilterContents, setLastFilterContents] = useState<string[]>(() =>
|
||||||
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
readSubComponentState(SubComponentName.FilterHistory, _collection, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
// For progress bar for bulk delete (noSql)
|
|
||||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = React.useState(false);
|
|
||||||
const [bulkDeleteProcess, setBulkDeleteProcess] = useState<{
|
|
||||||
pendingIds: DocumentId[];
|
|
||||||
successfulIds: DocumentId[];
|
|
||||||
throttledIds: DocumentId[];
|
|
||||||
failedIds: DocumentId[];
|
|
||||||
beforeExecuteMs: number; // Delay before executing delete. Used for retrying throttling after a specified delay
|
|
||||||
hasBeenThrottled: boolean; // Keep track if the operation has been throttled at least once
|
|
||||||
}>(undefined);
|
|
||||||
const [bulkDeleteOperation, setBulkDeleteOperation] = useState<{
|
|
||||||
onCompleted: (documentIds: DocumentId[]) => void;
|
|
||||||
onFailed: (reason?: unknown) => void;
|
|
||||||
count: number;
|
|
||||||
collection: CollectionBase;
|
|
||||||
}>(undefined);
|
|
||||||
const [bulkDeleteMode, setBulkDeleteMode] = useState<"inProgress" | "completed" | "aborting" | "aborted">(undefined);
|
|
||||||
|
|
||||||
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -680,99 +611,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
}, [documentIds, clickedRowIndex, editorState]);
|
}, [documentIds, clickedRowIndex, editorState]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively delete all documents by retrying throttled requests (429).
|
|
||||||
* This only works for NoSQL, because the bulk response includes status for each delete document request.
|
|
||||||
* Recursion is implemented using React useEffect (as opposed to recursively calling setTimeout), because it
|
|
||||||
* has to update the <ProgressModalDialog> or check if the user is aborting the operation via state React
|
|
||||||
* variables.
|
|
||||||
*
|
|
||||||
* Inputs are the bulkDeleteOperation, bulkDeleteProcess and bulkDeleteMode state variables.
|
|
||||||
* When the bulkDeleteProcess changes, the function in the useEffect is triggered and checks if the process
|
|
||||||
* was aborted or completed, which will resolve the promise.
|
|
||||||
* Otherwise, it will attempt to delete documents of the pending and throttled ids arrays.
|
|
||||||
* Once deletion is completed, the function updates bulkDeleteProcess with the results, which will trigger
|
|
||||||
* the function to be called again.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!bulkDeleteOperation || !bulkDeleteProcess || !bulkDeleteMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bulkDeleteMode === "completed" || bulkDeleteMode === "aborted") {
|
|
||||||
// no op in the case function is called again
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(bulkDeleteProcess.pendingIds.length === 0 && bulkDeleteProcess.throttledIds.length === 0) ||
|
|
||||||
bulkDeleteMode === "aborting"
|
|
||||||
) {
|
|
||||||
// Successfully deleted all documents or operation was aborted
|
|
||||||
bulkDeleteOperation.onCompleted(bulkDeleteProcess.successfulIds);
|
|
||||||
setBulkDeleteMode(bulkDeleteMode === "aborting" ? "aborted" : "completed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start deleting documents or retry throttled requests
|
|
||||||
const newPendingIds = bulkDeleteProcess.pendingIds.concat(bulkDeleteProcess.throttledIds);
|
|
||||||
const timeout = bulkDeleteProcess.beforeExecuteMs || 0;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
deleteNoSqlDocuments(bulkDeleteOperation.collection, [...newPendingIds])
|
|
||||||
.then((deleteResult) => {
|
|
||||||
let retryAfterMilliseconds = 0;
|
|
||||||
const newSuccessful: DocumentId[] = [];
|
|
||||||
const newThrottled: DocumentId[] = [];
|
|
||||||
const newFailed: DocumentId[] = [];
|
|
||||||
deleteResult.forEach((result) => {
|
|
||||||
if (result.statusCode === Constants.HttpStatusCodes.NoContent) {
|
|
||||||
newSuccessful.push(result.documentId);
|
|
||||||
} else if (result.statusCode === Constants.HttpStatusCodes.TooManyRequests) {
|
|
||||||
newThrottled.push(result.documentId);
|
|
||||||
retryAfterMilliseconds = Math.max(result.retryAfterMilliseconds, retryAfterMilliseconds);
|
|
||||||
} else if (result.statusCode >= 400) {
|
|
||||||
newFailed.push(result.documentId);
|
|
||||||
logConsoleError(
|
|
||||||
`Failed to delete document ${result.documentId.id} with status code ${result.statusCode}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logConsoleInfo(`Successfully deleted ${newSuccessful.length} document(s)`);
|
|
||||||
|
|
||||||
if (newThrottled.length > 0) {
|
|
||||||
logConsoleError(
|
|
||||||
`Failed to delete ${newThrottled.length} document(s) due to "Request too large" (429) error. Retrying...`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update result of the bulk delete: method is called again, because the state variables changed
|
|
||||||
// it will decide at the next call what to do
|
|
||||||
setBulkDeleteProcess((prev) => ({
|
|
||||||
pendingIds: [],
|
|
||||||
successfulIds: prev.successfulIds.concat(newSuccessful),
|
|
||||||
throttledIds: newThrottled,
|
|
||||||
failedIds: prev.failedIds.concat(newFailed),
|
|
||||||
beforeExecuteMs: retryAfterMilliseconds,
|
|
||||||
hasBeenThrottled: prev.hasBeenThrottled || newThrottled.length > 0,
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error deleting documents", error);
|
|
||||||
setBulkDeleteProcess((prev) => ({
|
|
||||||
pendingIds: [],
|
|
||||||
throttledIds: [],
|
|
||||||
successfulIds: prev.successfulIds,
|
|
||||||
failedIds: prev.failedIds.concat(prev.pendingIds),
|
|
||||||
beforeExecuteMs: undefined,
|
|
||||||
hasBeenThrottled: prev.hasBeenThrottled,
|
|
||||||
}));
|
|
||||||
bulkDeleteOperation.onFailed(error);
|
|
||||||
});
|
|
||||||
}, timeout);
|
|
||||||
}, [bulkDeleteOperation, bulkDeleteProcess, bulkDeleteMode]);
|
|
||||||
|
|
||||||
const applyFilterButton = {
|
const applyFilterButton = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -794,37 +632,10 @@ 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[]) =>
|
||||||
rawDocument: DataModels.DocumentId,
|
new DocumentId(
|
||||||
partitionKeyProperties: string[],
|
|
||||||
partitionKeyValue: string[],
|
|
||||||
): ExtendedDocumentId => {
|
|
||||||
const extendedDocumentId = new DocumentId(
|
|
||||||
{
|
{
|
||||||
partitionKey,
|
partitionKey,
|
||||||
partitionKeyProperties,
|
partitionKeyProperties,
|
||||||
@@ -834,10 +645,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
},
|
},
|
||||||
rawDocument,
|
rawDocument,
|
||||||
partitionKeyValue,
|
partitionKeyValue,
|
||||||
) as ExtendedDocumentId;
|
),
|
||||||
extendedDocumentId.tableFields = { ...rawDocument };
|
|
||||||
return extendedDocumentId;
|
|
||||||
},
|
|
||||||
[partitionKey],
|
[partitionKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -999,10 +807,6 @@ 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,
|
||||||
{
|
{
|
||||||
@@ -1028,10 +832,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => setSelectedRows(new Set([documentIds.length - 1])))
|
||||||
setSelectedRows(new Set([documentIds.length - 1]));
|
|
||||||
setClickedRowIndex(documentIds.length - 1);
|
|
||||||
})
|
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => setIsExecuting(false));
|
||||||
}, [
|
}, [
|
||||||
onExecutionErrorChange,
|
onExecutionErrorChange,
|
||||||
@@ -1088,10 +889,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
},
|
},
|
||||||
startKey,
|
startKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update column choices
|
|
||||||
selectedDocumentId.tableFields = { ...documentContent };
|
|
||||||
setColumnDefinitionsFromDocument(documentContent);
|
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
onExecutionErrorChange(true);
|
onExecutionErrorChange(true);
|
||||||
@@ -1125,36 +922,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
setSelectedDocumentContent(selectedDocumentContentBaseline);
|
setSelectedDocumentContent(selectedDocumentContentBaseline);
|
||||||
}, [initialDocumentContent, selectedDocumentContentBaseline, setSelectedDocumentContent]);
|
}, [initialDocumentContent, selectedDocumentContentBaseline, setSelectedDocumentContent]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger a useEffect() to bulk delete noSql documents
|
|
||||||
* @param collection
|
|
||||||
* @param documentIds
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const _bulkDeleteNoSqlDocuments = (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> =>
|
|
||||||
new Promise<DocumentId[]>((resolve, reject) => {
|
|
||||||
setBulkDeleteOperation({
|
|
||||||
onCompleted: resolve,
|
|
||||||
onFailed: reject,
|
|
||||||
count: documentIds.length,
|
|
||||||
collection,
|
|
||||||
});
|
|
||||||
setBulkDeleteProcess({
|
|
||||||
pendingIds: [...documentIds],
|
|
||||||
throttledIds: [],
|
|
||||||
successfulIds: [],
|
|
||||||
failedIds: [],
|
|
||||||
beforeExecuteMs: 0,
|
|
||||||
hasBeenThrottled: false,
|
|
||||||
});
|
|
||||||
setIsBulkDeleteDialogOpen(true);
|
|
||||||
setBulkDeleteMode("inProgress");
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation using bulk delete NoSQL API
|
* Implementation using bulk delete NoSQL API
|
||||||
* @param list of document ids to delete
|
|
||||||
* @returns Promise of list of deleted document ids
|
|
||||||
*/
|
*/
|
||||||
const _deleteDocuments = useCallback(
|
const _deleteDocuments = useCallback(
|
||||||
async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
|
async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
|
||||||
@@ -1165,33 +934,20 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
});
|
});
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
let deletePromise;
|
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
|
||||||
if (!isPreferredApiMongoDB) {
|
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should always be called.
|
||||||
if (partitionKey.systemKey) {
|
const _deleteNoSqlDocuments = async (
|
||||||
// ----------------------------------------------------------------------------------------------------
|
collection: CollectionBase,
|
||||||
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
|
toDeleteDocumentIds: DocumentId[],
|
||||||
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should
|
): Promise<DocumentId[]> => {
|
||||||
// always be called for NoSQL.
|
return partitionKey.systemKey
|
||||||
deletePromise = deleteNoSqlDocument(_collection, toDeleteDocumentIds[0]).then(() => {
|
? deleteNoSqlDocument(collection, toDeleteDocumentIds[0]).then(() => [toDeleteDocumentIds[0]])
|
||||||
useDialog.getState().showOkModalDialog("Delete document", "Document successfully deleted.");
|
: deleteNoSqlDocuments(collection, toDeleteDocumentIds);
|
||||||
return [toDeleteDocumentIds[0]];
|
};
|
||||||
});
|
|
||||||
// ----------------------------------------------------------------------------------------------------
|
const deletePromise = !isPreferredApiMongoDB
|
||||||
} else {
|
? _deleteNoSqlDocuments(_collection, toDeleteDocumentIds)
|
||||||
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
|
: MongoProxyClient.deleteDocuments(
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isMongoBulkDeleteDisabled) {
|
|
||||||
// TODO: Once new mongo proxy is available for all users, remove the call for MongoProxyClient.deleteDocument().
|
|
||||||
// MongoProxyClient.deleteDocuments() should be called for all users.
|
|
||||||
deletePromise = MongoProxyClient.deleteDocument(
|
|
||||||
_collection.databaseId,
|
|
||||||
_collection as ViewModels.Collection,
|
|
||||||
toDeleteDocumentIds[0],
|
|
||||||
).then(() => [toDeleteDocumentIds[0]]);
|
|
||||||
// ----------------------------------------------------------------------------------------------------
|
|
||||||
} else {
|
|
||||||
deletePromise = MongoProxyClient.deleteDocuments(
|
|
||||||
_collection.databaseId,
|
_collection.databaseId,
|
||||||
_collection as ViewModels.Collection,
|
_collection as ViewModels.Collection,
|
||||||
toDeleteDocumentIds,
|
toDeleteDocumentIds,
|
||||||
@@ -1201,8 +957,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return deletePromise
|
return deletePromise
|
||||||
.then(
|
.then(
|
||||||
@@ -1233,11 +987,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
throw error;
|
throw error;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(() => {
|
.finally(() => setIsExecuting(false));
|
||||||
setIsExecuting(false);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[_collection, isPreferredApiMongoDB, onExecutionErrorChange, tabTitle, partitionKey.systemKey],
|
[_collection, isPreferredApiMongoDB, onExecutionErrorChange, tabTitle],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteDocuments = useCallback(
|
const deleteDocuments = useCallback(
|
||||||
@@ -1255,25 +1007,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
setClickedRowIndex(undefined);
|
setClickedRowIndex(undefined);
|
||||||
setSelectedRows(new Set());
|
setSelectedRows(new Set());
|
||||||
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||||
|
useDialog
|
||||||
|
.getState()
|
||||||
|
.showOkModalDialog("Delete documents", `${deletedIds.length} document(s) successfully deleted.`);
|
||||||
},
|
},
|
||||||
(error: Error) => {
|
(error: Error) =>
|
||||||
if (error instanceof MongoProxyClient.ThrottlingError) {
|
useDialog
|
||||||
useDialog
|
.getState()
|
||||||
.getState()
|
.showOkModalDialog("Delete documents", `Deleting document(s) failed (${error.message})`),
|
||||||
.showOkModalDialog(
|
|
||||||
"Delete documents",
|
|
||||||
`Some documents failed to delete due to a rate limiting error. Please try again later. To prevent this in the future, consider increasing the throughput on your container or database.`,
|
|
||||||
{
|
|
||||||
linkText: "Learn More",
|
|
||||||
linkUrl: MONGO_THROTTLING_DOC_URL,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
useDialog
|
|
||||||
.getState()
|
|
||||||
.showOkModalDialog("Delete documents", `Deleting document(s) failed (${error.message})`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => setIsExecuting(false));
|
||||||
},
|
},
|
||||||
@@ -1349,13 +1090,7 @@ 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(
|
const query: string = buildQuery(isPreferredApiMongoDB, filter, partitionKeyProperties, partitionKey);
|
||||||
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'.
|
||||||
@@ -1378,7 +1113,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
resourceTokenPartitionKey,
|
resourceTokenPartitionKey,
|
||||||
isQueryCopilotSampleContainer,
|
isQueryCopilotSampleContainer,
|
||||||
_collection,
|
_collection,
|
||||||
selectedColumnIds,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onHideFilterClick = (): void => {
|
const onHideFilterClick = (): void => {
|
||||||
@@ -1524,6 +1258,16 @@ 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;
|
||||||
@@ -1555,7 +1299,9 @@ 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: DocumentsTableComponentItem = documentId.tableFields || { id: documentId.id() };
|
const item: Record<string, string> & { id: string } = {
|
||||||
|
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++) {
|
||||||
@@ -1566,44 +1312,6 @@ 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
|
||||||
@@ -1619,9 +1327,6 @@ 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);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1712,22 +1417,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
return () => resizeObserver.disconnect(); // clean up
|
return () => resizeObserver.disconnect(); // clean up
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Column definition is a map<id, ColumnDefinition> to garantee uniqueness
|
const columnHeaders = {
|
||||||
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() => {
|
idHeader: isPreferredApiMongoDB ? "_id" : "id",
|
||||||
const persistedColumnsSelection = readSubComponentState<ColumnsSelection>(
|
partitionKeyHeaders: (showPartitionKey(_collection, isPreferredApiMongoDB) && partitionKeyPropertyHeaders) || [],
|
||||||
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(() => {
|
||||||
@@ -1959,7 +1652,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, selectedColumnIds);
|
const query: string = buildQuery(isPreferredApiMongoDB, filter);
|
||||||
|
|
||||||
return MongoProxyClient.queryDocuments(
|
return MongoProxyClient.queryDocuments(
|
||||||
_collection.databaseId,
|
_collection.databaseId,
|
||||||
@@ -2025,7 +1718,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
const limitedLastFilterContents = lastFilterContents.slice(0, MAX_FILTER_HISTORY_COUNT);
|
||||||
|
|
||||||
setLastFilterContents(limitedLastFilterContents);
|
setLastFilterContents(limitedLastFilterContents);
|
||||||
saveSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
saveSubComponentState(SubComponentName.FilterHistory, _collection, lastFilterContents);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshDocumentsGrid = useCallback(
|
const refreshDocumentsGrid = useCallback(
|
||||||
@@ -2058,68 +1751,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
[createIterator, filterContent],
|
[createIterator, filterContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* While retrying, display: retrying now.
|
|
||||||
* If completed and all documents were deleted, display: all documents deleted.
|
|
||||||
* @returns 429 warning message
|
|
||||||
*/
|
|
||||||
const get429WarningMessageNoSql = (): string => {
|
|
||||||
let message = 'Some delete requests failed due to a "Request too large" exception (429)';
|
|
||||||
|
|
||||||
if (bulkDeleteOperation.count === bulkDeleteProcess.successfulIds.length) {
|
|
||||||
message += ", but were successfully retried.";
|
|
||||||
} else if (bulkDeleteMode === "inProgress" || bulkDeleteMode === "aborting") {
|
|
||||||
message += ". Retrying now.";
|
|
||||||
} else {
|
|
||||||
message += ".";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (message +=
|
|
||||||
" To prevent this in the future, consider increasing the throughput on your container or database.");
|
|
||||||
};
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
// TODO: remove isMongoBulkDeleteDisabled when new mongo proxy is enabled for all users
|
|
||||||
// TODO: remove partitionKey.systemKey when JS SDK bug is fixed
|
|
||||||
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint("bulkdelete");
|
|
||||||
const isBulkDeleteDisabled =
|
|
||||||
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
|
||||||
// -------------------------------------------------------
|
|
||||||
|
|
||||||
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" }}>
|
||||||
@@ -2169,7 +1800,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
<datalist id={`filtersList-${getUniqueId(_collection)}`}>
|
<datalist id={`filtersList-${getUniqueId(_collection)}`}>
|
||||||
{addStringsNoDuplicate(
|
{addStringsNoDuplicate(
|
||||||
lastFilterContents,
|
lastFilterContents,
|
||||||
isPreferredApiMongoDB ? defaultMongoFilters : getDefaultSqlFilters(partitionKeyProperties),
|
isPreferredApiMongoDB ? defaultMongoFilters : defaultSqlFilters,
|
||||||
).map((filter) => (
|
).map((filter) => (
|
||||||
<option key={filter} value={filter} />
|
<option key={filter} value={filter} />
|
||||||
))}
|
))}
|
||||||
@@ -2214,41 +1845,42 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
<Allotment
|
<Allotment
|
||||||
onDragEnd={(sizes: number[]) => {
|
onDragEnd={(sizes: number[]) => {
|
||||||
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]);
|
||||||
saveSubComponentState<TabDivider>(SubComponentName.MainTabDivider, _collection, tabStateData);
|
saveSubComponentState(SubComponentName.MainTabDivider, _collection, tabStateData);
|
||||||
setTabStateData(tabStateData);
|
setTabStateData(tabStateData);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
|
<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.tableContainer}>
|
<div className={styles.floatingControlsContainer}>
|
||||||
<div
|
<div className={styles.floatingControls}>
|
||||||
style={
|
<Button
|
||||||
{
|
appearance="transparent"
|
||||||
height: "100%",
|
aria-label="Refresh"
|
||||||
width: `calc(100% + ${calculateOffset(selectedColumnIds.length)}px)`,
|
size="small"
|
||||||
} /* Fix to make table not resize beyond parent's width */
|
icon={<ArrowClockwise16Filled />}
|
||||||
}
|
style={{
|
||||||
>
|
color: StyleConstants.AccentMedium,
|
||||||
<DocumentsTableComponent
|
}}
|
||||||
onRefreshTable={() => refreshDocumentsGrid(false)}
|
onClick={() => refreshDocumentsGrid(false)}
|
||||||
items={tableItems}
|
onKeyDown={onRefreshKeyInput}
|
||||||
onItemClicked={(index) => onDocumentClicked(index, documentIds)}
|
|
||||||
onSelectedRowsChange={onSelectedRowsChange}
|
|
||||||
selectedRows={selectedRows}
|
|
||||||
size={tableContainerSizePx}
|
|
||||||
selectedColumnIds={selectedColumnIds}
|
|
||||||
columnDefinitions={columnDefinitions}
|
|
||||||
isRowSelectionDisabled={
|
|
||||||
isBulkDeleteDisabled ||
|
|
||||||
(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)
|
||||||
|
}
|
||||||
|
collection={_collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{tableItems.length > 0 && (
|
{tableItems.length > 0 && (
|
||||||
<a
|
<a
|
||||||
className={styles.loadMore}
|
className={styles.loadMore}
|
||||||
@@ -2284,52 +1916,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
</Allotment>
|
</Allotment>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{bulkDeleteOperation && (
|
|
||||||
<ProgressModalDialog
|
|
||||||
isOpen={isBulkDeleteDialogOpen}
|
|
||||||
dismissText="Abort"
|
|
||||||
onDismiss={() => {
|
|
||||||
setIsBulkDeleteDialogOpen(false);
|
|
||||||
setBulkDeleteOperation(undefined);
|
|
||||||
}}
|
|
||||||
onCancel={() => setBulkDeleteMode("aborting")}
|
|
||||||
title={`Deleting ${bulkDeleteOperation.count} document(s)`}
|
|
||||||
message={`Successfully deleted ${bulkDeleteProcess.successfulIds.length} document(s).`}
|
|
||||||
maxValue={bulkDeleteOperation.count}
|
|
||||||
value={bulkDeleteProcess.successfulIds.length}
|
|
||||||
mode={bulkDeleteMode}
|
|
||||||
>
|
|
||||||
<div className={styles.deleteProgressContent}>
|
|
||||||
{(bulkDeleteMode === "aborting" || bulkDeleteMode === "aborted") && (
|
|
||||||
<div style={{ paddingBottom: tokens.spacingVerticalL }}>Deleting document(s) was aborted.</div>
|
|
||||||
)}
|
|
||||||
{(bulkDeleteProcess.failedIds.length > 0 ||
|
|
||||||
(bulkDeleteProcess.throttledIds.length > 0 && bulkDeleteMode !== "inProgress")) && (
|
|
||||||
<MessageBar intent="error" style={{ marginBottom: tokens.spacingVerticalL }}>
|
|
||||||
<MessageBarBody>
|
|
||||||
<MessageBarTitle>Error</MessageBarTitle>
|
|
||||||
Failed to delete{" "}
|
|
||||||
{bulkDeleteMode === "inProgress"
|
|
||||||
? bulkDeleteProcess.failedIds.length
|
|
||||||
: bulkDeleteProcess.failedIds.length + bulkDeleteProcess.throttledIds.length}{" "}
|
|
||||||
document(s).
|
|
||||||
</MessageBarBody>
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
{bulkDeleteProcess.hasBeenThrottled && (
|
|
||||||
<MessageBar intent="warning">
|
|
||||||
<MessageBarBody>
|
|
||||||
<MessageBarTitle>Warning</MessageBarTitle>
|
|
||||||
{get429WarningMessageNoSql()}{" "}
|
|
||||||
<Link href={NO_SQL_THROTTLING_DOC_URL} target="_blank">
|
|
||||||
Learn More
|
|
||||||
</Link>
|
|
||||||
</MessageBarBody>
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ProgressModalDialog>
|
|
||||||
)}
|
|
||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { deleteDocuments } 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/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import {
|
import {
|
||||||
DELETE_BUTTON_ID,
|
DELETE_BUTTON_ID,
|
||||||
DISCARD_BUTTON_ID,
|
DISCARD_BUTTON_ID,
|
||||||
@@ -49,9 +49,7 @@ jest.mock("Common/MongoProxyClient", () => ({
|
|||||||
id: "id1",
|
id: "id1",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
deleteDocuments: jest.fn(() => Promise.resolve({ deleteCount: 0, isAcknowledged: true })),
|
deleteDocuments: jest.fn(() => Promise.resolve()),
|
||||||
ThrottlingError: Error,
|
|
||||||
useMongoProxyEndpoint: jest.fn(() => true),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
||||||
@@ -163,7 +161,7 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -173,14 +171,14 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
.contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined();
|
||||||
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
expect(useCommandBar.getState().contextButtons.find((button) => button.id === DISCARD_BUTTON_ID)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking Delete Document eventually calls delete client api", () => {
|
it("clicking Delete Document asks for confirmation", () => {
|
||||||
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
||||||
mockDeleteDocuments.mockClear();
|
mockDeleteDocuments.mockClear();
|
||||||
|
|
||||||
@@ -188,7 +186,7 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
useCommandBar
|
useCommandBar
|
||||||
.getState()
|
.getState()
|
||||||
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
.contextButtons.find((button) => button.id === DELETE_BUTTON_ID)
|
||||||
.onCommandClick(undefined, undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockDeleteDocuments).toHaveBeenCalled();
|
expect(mockDeleteDocuments).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -21,19 +21,15 @@ describe("DocumentsTableComponent", () => {
|
|||||||
height: 0,
|
height: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
},
|
},
|
||||||
columnDefinitions: [
|
columnHeaders: {
|
||||||
{ id: ID_HEADER, label: "ID", isPartitionKey: false },
|
idHeader: ID_HEADER,
|
||||||
{ id: PARTITION_KEY_HEADER, label: "Partition Key", isPartitionKey: true },
|
partitionKeyHeaders: [PARTITION_KEY_HEADER],
|
||||||
],
|
},
|
||||||
isRowSelectionDisabled: false,
|
isSelectionDisabled: false,
|
||||||
collection: {
|
collection: {
|
||||||
databaseId: "db",
|
databaseId: "db",
|
||||||
id: ((): string => "coll") as ko.Observable<string>,
|
id: ((): string => "coll") as ko.Observable<string>,
|
||||||
} as ViewModels.CollectionBase,
|
} as ViewModels.CollectionBase,
|
||||||
onRefreshTable: (): void => {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
selectedColumnIds: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render documents and partition keys in header", () => {
|
it("should render documents and partition keys in header", () => {
|
||||||
@@ -44,7 +40,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.isRowSelectionDisabled = true;
|
props.isSelectionDisabled = true;
|
||||||
const wrapper = mount(<DocumentsTableComponent {...props} />);
|
const wrapper = mount(<DocumentsTableComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,49 +1,30 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
createTableColumn,
|
||||||
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,
|
||||||
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 { Environment, getEnvironment } from "Common/EnvironmentUtility";
|
|
||||||
import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane";
|
|
||||||
import {
|
import {
|
||||||
ColumnSizesMap,
|
ColumnSizesMap,
|
||||||
ColumnSort,
|
|
||||||
deleteSubComponentState,
|
|
||||||
readSubComponentState,
|
readSubComponentState,
|
||||||
saveSubComponentState,
|
saveSubComponentState,
|
||||||
SubComponentName,
|
SubComponentName,
|
||||||
@@ -52,35 +33,28 @@ import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs
|
|||||||
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 { 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";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
|
||||||
export type DocumentsTableComponentItem = {
|
export type DocumentsTableComponentItem = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Record<string, string | number>;
|
} & Record<string, string>;
|
||||||
|
|
||||||
export type ColumnDefinition = {
|
export type ColumnHeaders = {
|
||||||
id: string;
|
idHeader: string;
|
||||||
label: string;
|
partitionKeyHeaders: 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 };
|
||||||
selectedColumnIds: string[];
|
columnHeaders: ColumnHeaders;
|
||||||
columnDefinitions: ColumnDefinition[];
|
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
isRowSelectionDisabled?: boolean;
|
isSelectionDisabled?: boolean;
|
||||||
collection: ViewModels.CollectionBase;
|
collection: ViewModels.CollectionBase;
|
||||||
onColumnSelectionChange?: (newSelectedColumnIds: string[]) => void;
|
|
||||||
defaultColumnSelection?: string[];
|
|
||||||
isColumnSelectionDisabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
||||||
@@ -93,33 +67,25 @@ interface ReactWindowRenderFnProps extends ListChildComponentProps {
|
|||||||
data: TableRowData[];
|
data: TableRowData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLUMNS_MENU_NAME = "columnsMenu";
|
|
||||||
|
|
||||||
const defaultSize = {
|
const defaultSize = {
|
||||||
idealWidth: 200,
|
idealWidth: 200,
|
||||||
minWidth: 50,
|
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,
|
||||||
selectedColumnIds,
|
columnHeaders,
|
||||||
columnDefinitions,
|
isSelectionDisabled,
|
||||||
isRowSelectionDisabled: isSelectionDisabled,
|
|
||||||
collection,
|
collection,
|
||||||
onColumnSelectionChange,
|
|
||||||
defaultColumnSelection,
|
|
||||||
isColumnSelectionDisabled,
|
|
||||||
}: IDocumentsTableComponentProps) => {
|
}: IDocumentsTableComponentProps) => {
|
||||||
const styles = useDocumentsTabStyles();
|
|
||||||
|
|
||||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(() => {
|
||||||
|
const columnIds = ["id"].concat(columnHeaders.partitionKeyHeaders);
|
||||||
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {});
|
||||||
const columnSizesPx: TableColumnSizingOptions = {};
|
const columnSizesPx: TableColumnSizingOptions = {};
|
||||||
selectedColumnIds.forEach((columnId) => {
|
columnIds.forEach((columnId) => {
|
||||||
if (
|
if (
|
||||||
!columnSizesMap ||
|
!columnSizesMap ||
|
||||||
!columnSizesMap[columnId] ||
|
!columnSizesMap[columnId] ||
|
||||||
@@ -137,24 +103,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
return columnSizesPx;
|
return columnSizesPx;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sortState, setSortState] = React.useState<{
|
const styles = useDocumentsTabStyles();
|
||||||
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 onColumnResize = React.useCallback((_, { columnId, width }: { columnId: string; width: number }) => {
|
const onColumnResize = React.useCallback((_, { columnId, width }: { columnId: string; width: number }) => {
|
||||||
setColumnSizingOptions((state) => {
|
setColumnSizingOptions((state) => {
|
||||||
@@ -173,124 +122,42 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as ColumnSizesMap);
|
}, {} as ColumnSizesMap);
|
||||||
|
|
||||||
saveSubComponentState<ColumnSizesMap>(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
saveSubComponentState(SubComponentName.ColumnSizes, collection, persistentSizes, true);
|
||||||
|
|
||||||
return newSizingOptions;
|
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
|
[
|
||||||
.filter((column) => selectedColumnIds.includes(column.id))
|
createTableColumn<DocumentsTableComponentItem>({
|
||||||
.map((column) => ({
|
columnId: "id",
|
||||||
columnId: column.id,
|
compare: (a, b) => a.id.localeCompare(b.id),
|
||||||
compare: (a, b) => {
|
renderHeaderCell: () => columnHeaders.idHeader,
|
||||||
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>
|
|
||||||
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) && (
|
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) &&
|
|
||||||
!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[column.id]}`}>
|
<TableCellLayout truncate title={item.id}>
|
||||||
{item[column.id]}
|
{item.id}
|
||||||
</TableCellLayout>
|
</TableCellLayout>
|
||||||
),
|
),
|
||||||
})),
|
}),
|
||||||
[columnDefinitions, onColumnSelectionChange, selectedColumnIds],
|
].concat(
|
||||||
|
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);
|
||||||
@@ -380,7 +247,6 @@ 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,
|
||||||
@@ -394,36 +260,25 @@ 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 headerSortProps = (columnId: TableColumnId) => ({
|
const rows: TableRowData[] = getRows((row) => {
|
||||||
// onClick: (e: React.MouseEvent) => toggleColumnSort(e, columnId),
|
const selected = isRowSelected(row.rowId);
|
||||||
sortDirection: getSortDirection(columnId),
|
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 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 === " ") {
|
||||||
@@ -449,53 +304,39 @@ 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 {...tableProps}>
|
||||||
<TableHeader className={styles.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) => (
|
{columns.map((column /* index */) => (
|
||||||
<TableHeaderCell
|
<Menu openOnContext key={column.columnId}>
|
||||||
className={styles.tableCell}
|
<MenuTrigger>
|
||||||
key={column.columnId}
|
<TableHeaderCell
|
||||||
{...columnSizing.getTableHeaderCellProps(column.columnId)}
|
className={styles.tableCell}
|
||||||
{...headerSortProps(column.columnId)}
|
key={column.columnId}
|
||||||
>
|
{...columnSizing.getTableHeaderCellProps(column.columnId)}
|
||||||
{column.renderHeaderCell()}
|
>
|
||||||
</TableHeaderCell>
|
{column.renderHeaderCell()}
|
||||||
|
</TableHeaderCell>
|
||||||
|
</MenuTrigger>
|
||||||
|
<MenuPopover>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={columnSizing.enableKeyboardMode(column.columnId)}>
|
||||||
|
Keyboard Column Resizing
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</MenuPopover>
|
||||||
|
</Menu>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<div className={styles.tableHeaderFiller}></div>
|
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<List
|
<List
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
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.
|
||||||
@@ -92,12 +90,3 @@ 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;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -55,57 +55,53 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="___9o87uj0_0000000 ffefeo0"
|
className="___77lcry0_0000000 f10pi13n"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={
|
className="___1rwkz4r_0000000 f1euv43f f1l8gmrm f1e31b4d f150nix6 fy6ml6n f19g0ac"
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"width": "calc(100% + -11px)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<DocumentsTableComponent
|
<Button
|
||||||
collection={
|
appearance="transparent"
|
||||||
|
aria-label="Refresh"
|
||||||
|
icon={<ArrowClockwise16Filled />}
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
size="small"
|
||||||
|
style={
|
||||||
{
|
{
|
||||||
"databaseId": "databaseId",
|
"color": undefined,
|
||||||
"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
|
||||||
|
collection={
|
||||||
|
{
|
||||||
|
"databaseId": "databaseId",
|
||||||
|
"id": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -152,6 +152,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
ariaLabel: saveLabel,
|
ariaLabel: saveLabel,
|
||||||
children: saveButtonChildren.length && [
|
children: saveButtonChildren.length && [
|
||||||
{
|
{
|
||||||
|
iconName: "Save",
|
||||||
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||||
commandButtonLabel: saveLabel,
|
commandButtonLabel: saveLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
createTableColumn,
|
createTableColumn,
|
||||||
tokens,
|
tokens,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { ErrorCircleFilled, MoreHorizontalRegular, QuestionRegular, WarningFilled } from "@fluentui/react-icons";
|
import { ErrorCircleFilled, MoreHorizontalRegular, WarningFilled } from "@fluentui/react-icons";
|
||||||
import QueryError, { QueryErrorSeverity, compareSeverity } from "Common/QueryError";
|
import QueryError, { QueryErrorSeverity, compareSeverity } from "Common/QueryError";
|
||||||
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
||||||
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
||||||
@@ -34,32 +34,25 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
|
|||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "code",
|
columnId: "code",
|
||||||
compare: (item1, item2) => item1.code.localeCompare(item2.code),
|
compare: (item1, item2) => item1.code.localeCompare(item2.code),
|
||||||
renderHeaderCell: () => "Code",
|
renderHeaderCell: () => null,
|
||||||
renderCell: (item) => <TableCellLayout truncate>{item.code}</TableCellLayout>,
|
renderCell: (item) => item.code,
|
||||||
}),
|
}),
|
||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "severity",
|
columnId: "severity",
|
||||||
compare: (item1, item2) => compareSeverity(item1.severity, item2.severity),
|
compare: (item1, item2) => compareSeverity(item1.severity, item2.severity),
|
||||||
renderHeaderCell: () => "Severity",
|
renderHeaderCell: () => null,
|
||||||
renderCell: (item) => (
|
renderCell: (item) => <TableCellLayout media={severityIcons[item.severity]}>{item.severity}</TableCellLayout>,
|
||||||
<TableCellLayout truncate media={severityIcons[item.severity]}>
|
|
||||||
{item.severity}
|
|
||||||
</TableCellLayout>
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "location",
|
columnId: "location",
|
||||||
compare: (item1, item2) => item1.location?.start?.offset - item2.location?.start?.offset,
|
compare: (item1, item2) => item1.location?.start?.offset - item2.location?.start?.offset,
|
||||||
renderHeaderCell: () => "Location",
|
renderHeaderCell: () => "Location",
|
||||||
renderCell: (item) => (
|
renderCell: (item) =>
|
||||||
<TableCellLayout truncate>
|
item.location
|
||||||
{item.location
|
? item.location.start.lineNumber
|
||||||
? item.location.start.lineNumber
|
? `Line ${item.location.start.lineNumber}`
|
||||||
? `Line ${item.location.start.lineNumber}`
|
: "<unknown>"
|
||||||
: "<unknown>"
|
: "<no location>",
|
||||||
: "<no location>"}
|
|
||||||
</TableCellLayout>
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
createTableColumn<QueryError>({
|
createTableColumn<QueryError>({
|
||||||
columnId: "message",
|
columnId: "message",
|
||||||
@@ -67,20 +60,8 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
|
|||||||
renderHeaderCell: () => "Message",
|
renderHeaderCell: () => "Message",
|
||||||
renderCell: (item) => (
|
renderCell: (item) => (
|
||||||
<div className={styles.errorListMessageCell}>
|
<div className={styles.errorListMessageCell}>
|
||||||
<div className={styles.errorListMessage} title={item.message}>
|
<div className={styles.errorListMessage}>{item.message}</div>
|
||||||
{item.message}
|
<div>
|
||||||
</div>
|
|
||||||
<div className={styles.errorListMessageActions}>
|
|
||||||
{item.helpLink && (
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
aria-label="Help"
|
|
||||||
appearance="subtle"
|
|
||||||
icon={<QuestionRegular />}
|
|
||||||
href={item.helpLink}
|
|
||||||
target="_blank"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
aria-label="Details"
|
aria-label="Details"
|
||||||
appearance="subtle"
|
appearance="subtle"
|
||||||
@@ -95,9 +76,9 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
|
|||||||
|
|
||||||
const columnSizingOptions: TableColumnSizingOptions = {
|
const columnSizingOptions: TableColumnSizingOptions = {
|
||||||
code: {
|
code: {
|
||||||
minWidth: 90,
|
minWidth: 75,
|
||||||
idealWidth: 90,
|
idealWidth: 75,
|
||||||
defaultWidth: 90,
|
defaultWidth: 75,
|
||||||
},
|
},
|
||||||
severity: {
|
severity: {
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import { fireEvent, render } from "@testing-library/react";
|
|||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
import { CopilotSubComponentNames } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
|
||||||
import {
|
import {
|
||||||
IQueryTabComponentProps,
|
IQueryTabComponentProps,
|
||||||
QueryTabComponent,
|
QueryTabComponent,
|
||||||
QueryTabCopilotComponent,
|
QueryTabCopilotComponent,
|
||||||
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
import { AppStateComponentNames, StorePath } from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { updateUserContext, userContext } from "UserContext";
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -18,24 +16,6 @@ import React from "react";
|
|||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact");
|
jest.mock("Explorer/Controls/Editor/EditorReact");
|
||||||
|
|
||||||
const loadState = (path: StorePath) => {
|
|
||||||
if (
|
|
||||||
path.componentName === AppStateComponentNames.QueryCopilot &&
|
|
||||||
path.subComponentName === CopilotSubComponentNames.toggleStatus
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
|
||||||
loadState,
|
|
||||||
AppStateComponentNames: {
|
|
||||||
QueryCopilot: "QueryCopilot",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("QueryTabComponent", () => {
|
describe("QueryTabComponent", () => {
|
||||||
const mockStore = useQueryCopilot.getState();
|
const mockStore = useQueryCopilot.getState();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -52,7 +32,7 @@ describe("QueryTabComponent", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const propsMock: Readonly<IQueryTabComponentProps> = {
|
const propsMock: Readonly<IQueryTabComponentProps> = {
|
||||||
collection: { databaseId: "CopilotSampleDB" },
|
collection: { databaseId: "CopilotSampleDb" },
|
||||||
onTabAccessor: () => jest.fn(),
|
onTabAccessor: () => jest.fn(),
|
||||||
isExecutionError: false,
|
isExecutionError: false,
|
||||||
tabId: "mockTabId",
|
tabId: "mockTabId",
|
||||||
@@ -70,17 +50,6 @@ describe("QueryTabComponent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("copilot should be enabled by default when tab is active", () => {
|
it("copilot should be enabled by default when tab is active", () => {
|
||||||
updateUserContext({
|
|
||||||
databaseAccount: {
|
|
||||||
name: "name",
|
|
||||||
properties: undefined,
|
|
||||||
id: "",
|
|
||||||
location: "",
|
|
||||||
type: "",
|
|
||||||
kind: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useQueryCopilot.getState().setCopilotEnabled(true);
|
useQueryCopilot.getState().setCopilotEnabled(true);
|
||||||
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
|
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
|
||||||
const activeTab = new TabsBase({
|
const activeTab = new TabsBase({
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import { SplitterDirection } from "Common/Splitter";
|
|||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { monaco } from "Explorer/LazyMonaco";
|
import { monaco } from "Explorer/LazyMonaco";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
|
||||||
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
@@ -48,6 +46,7 @@ import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
|||||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import * as StringUtility from "../../../Shared/StringUtility";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
@@ -55,6 +54,7 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
|||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
|
||||||
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
@@ -209,7 +209,13 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
|
|
||||||
private _queryCopilotActive(): boolean {
|
private _queryCopilotActive(): boolean {
|
||||||
if (this.props.copilotEnabled) {
|
if (this.props.copilotEnabled) {
|
||||||
return readCopilotToggleStatus(userContext.databaseAccount);
|
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
||||||
|
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
||||||
|
);
|
||||||
|
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
||||||
|
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
||||||
|
: true;
|
||||||
|
return copilotInitialActive;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -578,7 +584,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
private _toggleCopilot = (active: boolean) => {
|
private _toggleCopilot = (active: boolean) => {
|
||||||
this.setState({ copilotActive: active });
|
this.setState({ copilotActive: active });
|
||||||
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
|
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
|
||||||
saveCopilotToggleStatus(userContext.databaseAccount, active);
|
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, active.toString());
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
|
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
|
||||||
databaseName: this.props.collection.databaseId,
|
databaseName: this.props.collection.databaseId,
|
||||||
|
|||||||
@@ -72,11 +72,6 @@ export const useQueryTabStyles = makeStyles({
|
|||||||
metricsGridButtons: {
|
metricsGridButtons: {
|
||||||
...cosmosShorthands.borderTop(),
|
...cosmosShorthands.borderTop(),
|
||||||
},
|
},
|
||||||
errorListTableCell: {
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
},
|
|
||||||
errorListMessageCell: {
|
errorListMessageCell: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -85,12 +80,5 @@ export const useQueryTabStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
errorListMessage: {
|
errorListMessage: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
},
|
|
||||||
errorListMessageActions: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||||
import { Pivot, PivotItem } from "@fluentui/react";
|
import { Pivot, PivotItem } from "@fluentui/react";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
@@ -16,6 +15,7 @@ import { useTabs } from "../../../hooks/useTabs";
|
|||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import ScriptTabBase from "../ScriptTabBase";
|
import ScriptTabBase from "../ScriptTabBase";
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { IMessageBarStyles, 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 { configContext } from "ConfigContext";
|
import { Platform, configContext, updateConfigContext } 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";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { CommandBarV2 } from "Explorer/Menus/CommandBarV2/CommandBarV2";
|
|
||||||
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
||||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||||
@@ -17,6 +16,7 @@ import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
|||||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
|
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
@@ -37,6 +37,9 @@ interface TabsProps {
|
|||||||
|
|
||||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
||||||
|
const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState<boolean>(
|
||||||
|
userContext.apiType === "SQL" && configContext.platform !== Platform.Fabric && !hasRUThresholdBeenConfigured(),
|
||||||
|
);
|
||||||
const [
|
const [
|
||||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||||
@@ -84,6 +87,30 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
{networkSettingsWarning}
|
{networkSettingsWarning}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
|
{showRUThresholdMessageBar && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={MessageBarType.info}
|
||||||
|
onDismiss={() => {
|
||||||
|
setShowRUThresholdMessageBar(false);
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
...defaultMessageBarStyles,
|
||||||
|
innerText: {
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove
|
||||||
|
the limit, go to the Settings cog on the right and find "RU Threshold".`}
|
||||||
|
<Link
|
||||||
|
className="underlinedLink"
|
||||||
|
href="https://review.learn.microsoft.com/en-us/azure/cosmos-db/data-explorer?branch=main#configure-request-unit-threshold"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</Link>
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarType={MessageBarType.warning}
|
messageBarType={MessageBarType.warning}
|
||||||
@@ -92,7 +119,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{`We have migrated our middleware to new infrastructure. To avoid issues with Data Explorer access, please
|
{`We are moving our middleware to new infrastructure. To avoid future 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>
|
||||||
)}
|
)}
|
||||||
@@ -107,7 +134,6 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="tabPanesContainer">
|
<div className="tabPanesContainer">
|
||||||
{userContext.features.commandBarV2 && <CommandBarV2 explorer={explorer} />}
|
|
||||||
{activeReactTab !== undefined && getReactTabContent(activeReactTab, explorer)}
|
{activeReactTab !== undefined && getReactTabContent(activeReactTab, explorer)}
|
||||||
{openedTabs.map((tab) => (
|
{openedTabs.map((tab) => (
|
||||||
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||||
@@ -372,6 +398,12 @@ 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 = [
|
||||||
@@ -390,6 +422,12 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as ThemeUtility from "../../Common/ThemeUtility";
|
import * as ThemeUtility from "../../Common/ThemeUtility";
|
||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -10,6 +10,7 @@ import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
|||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
// TODO: Use specific actions for logging telemetry data
|
// TODO: Use specific actions for logging telemetry data
|
||||||
@@ -27,6 +28,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
public tabPath: ko.Observable<string>;
|
public tabPath: ko.Observable<string>;
|
||||||
public isExecutionError = ko.observable(false);
|
public isExecutionError = ko.observable(false);
|
||||||
public isExecuting = ko.observable(false);
|
public isExecuting = ko.observable(false);
|
||||||
|
public pendingNotification?: ko.Observable<DataModels.Notification>;
|
||||||
protected _theme: string;
|
protected _theme: string;
|
||||||
public onLoadStartKey: number;
|
public onLoadStartKey: number;
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
this.tabPath =
|
this.tabPath =
|
||||||
this.collection &&
|
this.collection &&
|
||||||
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${options.title}`);
|
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${options.title}`);
|
||||||
|
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
|
||||||
this.onLoadStartKey = options.onLoadStartKey;
|
this.onLoadStartKey = options.onLoadStartKey;
|
||||||
this.closeTabButton = {
|
this.closeTabButton = {
|
||||||
enabled: ko.computed<boolean>(() => {
|
enabled: ko.computed<boolean>(() => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { TriggerDefinition } from "@azure/cosmos";
|
import { TriggerDefinition } from "@azure/cosmos";
|
||||||
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
@@ -15,6 +14,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|||||||
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../Controls/Editor/EditorReact";
|
import { EditorReact } from "../Controls/Editor/EditorReact";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import TriggerTab from "./TriggerTab";
|
import TriggerTab from "./TriggerTab";
|
||||||
|
|
||||||
const triggerTypeOptions: IDropdownOption[] = [
|
const triggerTypeOptions: IDropdownOption[] = [
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import { Label, TextField } from "@fluentui/react";
|
import { Label, TextField } from "@fluentui/react";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
@@ -14,6 +13,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
|||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "../Controls/Editor/EditorReact";
|
import { EditorReact } from "../Controls/Editor/EditorReact";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import UserDefinedFunctionTab from "./UserDefinedFunctionTab";
|
import UserDefinedFunctionTab from "./UserDefinedFunctionTab";
|
||||||
|
|
||||||
interface IUserDefinedFunctionTabContentState {
|
interface IUserDefinedFunctionTabContentState {
|
||||||
|
|||||||
@@ -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: 32,
|
rowHeight: 36,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Our CosmosFluentProvider has the same props as a FluentProvider.
|
// Our CosmosFluentProvider has the same props as a FluentProvider.
|
||||||
@@ -91,30 +91,15 @@ const appThemePortalBrandRamp: BrandVariants = {
|
|||||||
160: "#CDD8EF",
|
160: "#CDD8EF",
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum LayoutSize {
|
const cosmosThemeElements = {
|
||||||
Compact,
|
layoutRowHeight: `${LayoutConstants.rowHeight}px`,
|
||||||
// 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 const tokens = themeToTokensObject({ ...webLightTheme, ...cosmosTheme, ...sizeMappings[LayoutSize.Compact] });
|
export type CosmosTheme = Theme & typeof cosmosThemeElements;
|
||||||
|
|
||||||
|
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),
|
||||||
@@ -132,7 +117,6 @@ export function getPlatformTheme(platform: Platform): CosmosTheme {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...baseTheme,
|
...baseTheme,
|
||||||
...cosmosTheme,
|
...cosmosThemeElements,
|
||||||
...sizeMappings[LayoutSize.Compact], // TODO: Allow for different layout sizes.
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import * as Logger from "../../Common/Logger";
|
||||||
|
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||||
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
|
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
||||||
@@ -24,6 +25,7 @@ import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
|||||||
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
||||||
import ConflictsTab from "../Tabs/ConflictsTab";
|
import ConflictsTab from "../Tabs/ConflictsTab";
|
||||||
import GraphTab from "../Tabs/GraphTab";
|
import GraphTab from "../Tabs/GraphTab";
|
||||||
@@ -1018,6 +1020,41 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.uploadFiles(event.originalEvent.dataTransfer.files);
|
this.uploadFiles(event.originalEvent.dataTransfer.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
|
||||||
|
if (!this.container) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const notifications: DataModels.Notification[] = await fetchPortalNotifications();
|
||||||
|
if (!notifications || notifications.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.find(notifications, (notification: DataModels.Notification) => {
|
||||||
|
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
|
||||||
|
return (
|
||||||
|
notification.kind === "message" &&
|
||||||
|
notification.collectionName === this.id() &&
|
||||||
|
notification.description &&
|
||||||
|
throughputUpdateRegExp.test(notification.description)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(
|
||||||
|
JSON.stringify({
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
accountName: userContext?.databaseAccount,
|
||||||
|
databaseName: this.databaseId,
|
||||||
|
collectionName: this.id(),
|
||||||
|
}),
|
||||||
|
"Settings tree node",
|
||||||
|
);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
|
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
|
||||||
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
|
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import * as _ from "underscore";
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import * as Logger from "../../Common/Logger";
|
||||||
|
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||||
import { readCollections, readCollectionsWithPagination } from "../../Common/dataAccess/readCollections";
|
import { readCollections, readCollectionsWithPagination } from "../../Common/dataAccess/readCollections";
|
||||||
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
@@ -74,6 +76,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
await useDatabases.getState().loadAllOffers();
|
await useDatabases.getState().loadAllOffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification();
|
||||||
const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2;
|
const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2;
|
||||||
const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id());
|
const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id());
|
||||||
let settingsTab = matchingTabs?.[0] as DatabaseSettingsTabV2;
|
let settingsTab = matchingTabs?.[0] as DatabaseSettingsTabV2;
|
||||||
@@ -84,39 +87,53 @@ export default class Database implements ViewModels.Database {
|
|||||||
dataExplorerArea: Constants.Areas.Tab,
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
tabTitle: "Scale",
|
tabTitle: "Scale",
|
||||||
});
|
});
|
||||||
|
pendingNotificationsPromise.then(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(data: any) => {
|
||||||
|
const pendingNotification: DataModels.Notification = data?.[0];
|
||||||
|
const tabOptions: ViewModels.TabOptions = {
|
||||||
|
tabKind,
|
||||||
|
title: "Scale",
|
||||||
|
tabPath: "",
|
||||||
|
node: this,
|
||||||
|
rid: this.rid,
|
||||||
|
database: this,
|
||||||
|
onLoadStartKey: startKey,
|
||||||
|
};
|
||||||
|
settingsTab = new DatabaseSettingsTabV2(tabOptions);
|
||||||
|
settingsTab.pendingNotification(pendingNotification);
|
||||||
|
useTabs.getState().activateNewTab(settingsTab);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
TelemetryProcessor.traceFailure(
|
||||||
|
Action.Tab,
|
||||||
|
{
|
||||||
|
databaseName: this.id(),
|
||||||
|
collectionName: this.id(),
|
||||||
|
|
||||||
try {
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
const tabOptions: ViewModels.TabOptions = {
|
tabTitle: "Scale",
|
||||||
tabKind,
|
error: errorMessage,
|
||||||
title: "Scale",
|
errorStack: getErrorStack(error),
|
||||||
tabPath: "",
|
},
|
||||||
node: this,
|
startKey,
|
||||||
rid: this.rid,
|
);
|
||||||
database: this,
|
logConsoleError(`Error while fetching database settings for database ${this.id()}: ${errorMessage}`);
|
||||||
onLoadStartKey: startKey,
|
throw error;
|
||||||
};
|
},
|
||||||
settingsTab = new DatabaseSettingsTabV2(tabOptions);
|
);
|
||||||
useTabs.getState().activateNewTab(settingsTab);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = getErrorMessage(error);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.Tab,
|
|
||||||
{
|
|
||||||
databaseName: this.id(),
|
|
||||||
collectionName: this.id(),
|
|
||||||
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: "Scale",
|
|
||||||
error: errorMessage,
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
logConsoleError(`Error while fetching database settings for database ${this.id()}: ${errorMessage}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
useTabs.getState().activateTab(settingsTab);
|
pendingNotificationsPromise.then(
|
||||||
|
(pendingNotification: DataModels.Notification) => {
|
||||||
|
settingsTab.pendingNotification(pendingNotification);
|
||||||
|
useTabs.getState().activateTab(settingsTab);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
settingsTab.pendingNotification(undefined);
|
||||||
|
useTabs.getState().activateTab(settingsTab);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,6 +260,42 @@ export default class Database implements ViewModels.Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
|
||||||
|
if (!this.container) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const notifications: DataModels.Notification[] = await fetchPortalNotifications();
|
||||||
|
if (!notifications || notifications.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.find(notifications, (notification: DataModels.Notification) => {
|
||||||
|
const throughputUpdateRegExp = new RegExp("Throughput update (.*) in progress");
|
||||||
|
return (
|
||||||
|
notification.kind === "message" &&
|
||||||
|
!notification.collectionName &&
|
||||||
|
notification.databaseName === this.id() &&
|
||||||
|
notification.description &&
|
||||||
|
throughputUpdateRegExp.test(notification.description)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(
|
||||||
|
JSON.stringify({
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
accountName: userContext?.databaseAccount,
|
||||||
|
databaseName: this.id(),
|
||||||
|
collectionName: this.id(),
|
||||||
|
}),
|
||||||
|
"Settings tree node",
|
||||||
|
);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getDeltaCollections(updatedCollectionsList: DataModels.Collection[]): {
|
private getDeltaCollections(updatedCollectionsList: DataModels.Collection[]): {
|
||||||
toAdd: DataModels.Collection[];
|
toAdd: DataModels.Collection[];
|
||||||
toDelete: Collection[];
|
toDelete: Collection[];
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
|
||||||
import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil";
|
import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil";
|
||||||
import { getItemName } from "Utils/APITypeUtils";
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
@@ -29,6 +27,8 @@ import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFacto
|
|||||||
import { useDialog } from "../Controls/Dialog";
|
import { useDialog } from "../Controls/Dialog";
|
||||||
import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent";
|
import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
@@ -229,7 +229,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
||||||
},
|
},
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ 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",
|
||||||
@@ -72,9 +69,6 @@ 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",
|
||||||
@@ -98,9 +92,6 @@ 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",
|
||||||
@@ -111,9 +102,6 @@ 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",
|
||||||
@@ -145,9 +133,6 @@ 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",
|
||||||
@@ -171,9 +156,6 @@ 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",
|
||||||
@@ -264,9 +246,6 @@ 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",
|
||||||
@@ -295,9 +274,6 @@ 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",
|
||||||
@@ -369,9 +345,6 @@ 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",
|
||||||
@@ -442,9 +415,6 @@ 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",
|
||||||
@@ -468,9 +438,6 @@ 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",
|
||||||
@@ -481,9 +448,6 @@ 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",
|
||||||
@@ -546,9 +510,6 @@ 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",
|
||||||
@@ -572,9 +533,6 @@ 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",
|
||||||
@@ -696,9 +654,6 @@ 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",
|
||||||
@@ -727,9 +682,6 @@ 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",
|
||||||
@@ -754,9 +706,6 @@ 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",
|
||||||
@@ -775,9 +724,6 @@ 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",
|
||||||
@@ -801,9 +747,6 @@ 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",
|
||||||
@@ -823,9 +766,6 @@ 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",
|
||||||
@@ -849,9 +789,6 @@ 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",
|
||||||
@@ -871,9 +808,6 @@ 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",
|
||||||
@@ -902,9 +836,6 @@ 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",
|
||||||
@@ -1045,9 +976,6 @@ 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",
|
||||||
@@ -1148,9 +1076,6 @@ 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",
|
||||||
@@ -1174,9 +1099,6 @@ 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",
|
||||||
@@ -1187,9 +1109,6 @@ 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",
|
||||||
@@ -1282,9 +1201,6 @@ 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",
|
||||||
@@ -1308,9 +1224,6 @@ 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",
|
||||||
@@ -1462,9 +1375,6 @@ 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",
|
||||||
@@ -1493,9 +1403,6 @@ 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",
|
||||||
@@ -1636,9 +1543,6 @@ 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",
|
||||||
@@ -1734,9 +1638,6 @@ 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",
|
||||||
@@ -1760,9 +1661,6 @@ 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",
|
||||||
@@ -1773,9 +1671,6 @@ 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",
|
||||||
@@ -1868,9 +1763,6 @@ 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",
|
||||||
@@ -1894,9 +1786,6 @@ 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",
|
||||||
@@ -2048,9 +1937,6 @@ 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",
|
||||||
@@ -2079,9 +1965,6 @@ 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",
|
||||||
@@ -2103,9 +1986,6 @@ 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",
|
||||||
@@ -2141,9 +2021,6 @@ 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",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CapabilityNames } from "Common/Constants";
|
|||||||
import { Platform, updateConfigContext } from "ConfigContext";
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel";
|
import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel";
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
|
|||||||
@@ -1,7 +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 { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
|
||||||
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
|
|
||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
||||||
import Trigger from "Explorer/Tree/Trigger";
|
import Trigger from "Explorer/Tree/Trigger";
|
||||||
@@ -10,7 +7,6 @@ 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";
|
||||||
@@ -18,6 +14,8 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
|||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
|
||||||
@@ -27,10 +25,6 @@ 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,
|
||||||
@@ -42,7 +36,6 @@ 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([]);
|
||||||
@@ -98,7 +91,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.onDocumentDBDocumentsClick();
|
collection.onDocumentDBDocumentsClick();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
||||||
},
|
},
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
@@ -111,7 +104,6 @@ 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);
|
||||||
@@ -141,7 +133,6 @@ 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()
|
||||||
@@ -178,7 +169,6 @@ 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) {
|
||||||
@@ -229,12 +219,11 @@ 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();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
||||||
},
|
},
|
||||||
onExpanded: async () => {
|
onExpanded: async () => {
|
||||||
// Rewritten version of expandCollapseCollection
|
// Rewritten version of expandCollapseCollection
|
||||||
@@ -282,7 +271,7 @@ const buildCollectionNodeChildren = (
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
||||||
},
|
},
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
@@ -20,11 +20,9 @@ import "../externals/jquery.typeahead.min.css";
|
|||||||
import "../externals/jquery.typeahead.min.js";
|
import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
import { Platform } from "ConfigContext";
|
import { Platform } from "ConfigContext";
|
||||||
import { CommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
import { SidebarContainer } from "Explorer/Sidebar";
|
import { SidebarContainer } from "Explorer/Sidebar";
|
||||||
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
import { KeyboardShortcutRoot } from "KeyboardShortcuts";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import "allotment/dist/style.css";
|
import "allotment/dist/style.css";
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
@@ -50,6 +48,7 @@ import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
|||||||
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
||||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||||
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
|
||||||
|
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
|
import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
|
||||||
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
|
||||||
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
|
||||||
@@ -87,7 +86,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||||
<div id="freeTierTeachingBubble"> </div>
|
<div id="freeTierTeachingBubble"> </div>
|
||||||
{/* Main Command Bar - Start */}
|
{/* Main Command Bar - Start */}
|
||||||
{!userContext.features.commandBarV2 && <CommandBar container={explorer} />}
|
<CommandBar container={explorer} />
|
||||||
{/* Collections Tree and Tabs - Begin */}
|
{/* Collections Tree and Tabs - Begin */}
|
||||||
<SidebarContainer explorer={explorer} />
|
<SidebarContainer explorer={explorer} />
|
||||||
{/* Collections Tree and Tabs - End */}
|
{/* Collections Tree and Tabs - End */}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { CommandButtonComponent } from "../../../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import FeedbackIcon from "../../../../images/Feedback.svg";
|
import FeedbackIcon from "../../../../images/Feedback.svg";
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FeedbackCommandButton: React.FunctionComponent = () => {
|
export const FeedbackCommandButton: React.FunctionComponent = () => {
|
||||||
return (
|
return (
|
||||||
<div className="feedbackConnectSettingIcons">
|
<div className="feedbackConnectSettingIcons">
|
||||||
<div className="commandButtonReact">
|
<CommandButtonComponent
|
||||||
<a href="#" title="Send feedback" aria-haspopup="dialog" onClick={onClick}>
|
id="commandbutton-feedback"
|
||||||
<img src={FeedbackIcon} alt="Send feedback" />
|
iconSrc={FeedbackIcon}
|
||||||
</a>
|
iconAlt="feeback button"
|
||||||
</div>
|
onCommandClick={() =>
|
||||||
|
window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback")
|
||||||
|
}
|
||||||
|
ariaLabel="feeback button"
|
||||||
|
tooltipText="Send feedback"
|
||||||
|
hasPopup={true}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ describe("parseResourceTokenConnectionString", () => {
|
|||||||
collectionId: "fakeCollectionId",
|
collectionId: "fakeCollectionId",
|
||||||
databaseId: "fakeDatabaseId",
|
databaseId: "fakeDatabaseId",
|
||||||
partitionKey: undefined,
|
partitionKey: undefined,
|
||||||
resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken",
|
resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ describe("parseResourceTokenConnectionString", () => {
|
|||||||
collectionId: "fakeCollectionId",
|
collectionId: "fakeCollectionId",
|
||||||
databaseId: "fakeDatabaseId",
|
databaseId: "fakeDatabaseId",
|
||||||
partitionKey: "fakePartitionKey",
|
partitionKey: "fakePartitionKey",
|
||||||
resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken",
|
resourceToken: "type=resource&ver=1&sig=2dIP+CdIfT1ScwHWdv5GGw==;fakeToken;",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ export function parseResourceTokenConnectionString(connectionString: string): Pa
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resourceToken && resourceToken.endsWith(";")) {
|
|
||||||
resourceToken = resourceToken.substring(0, resourceToken.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accountEndpoint,
|
accountEndpoint,
|
||||||
collectionId,
|
collectionId,
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export type Features = {
|
|||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
readonly commandBarV2: boolean;
|
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -109,7 +108,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
commandBarV2: "true" === get("commandbarv2"),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { createKeyFromPath, deleteState, loadState, MAX_ENTRY_NB, saveState } from "Shared/AppStatePersistenceUtility";
|
||||||
AppStateComponentNames,
|
|
||||||
createKeyFromPath,
|
|
||||||
deleteState,
|
|
||||||
loadState,
|
|
||||||
MAX_ENTRY_NB,
|
|
||||||
PATH_SEPARATOR,
|
|
||||||
saveState,
|
|
||||||
} from "Shared/AppStatePersistenceUtility";
|
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
|
||||||
jest.mock("Shared/StorageUtility", () => ({
|
jest.mock("Shared/StorageUtility", () => ({
|
||||||
@@ -21,7 +13,7 @@ jest.mock("Shared/StorageUtility", () => ({
|
|||||||
|
|
||||||
describe("AppStatePersistenceUtility", () => {
|
describe("AppStatePersistenceUtility", () => {
|
||||||
const storePath = {
|
const storePath = {
|
||||||
componentName: AppStateComponentNames.DocumentsTab,
|
componentName: "a",
|
||||||
subComponentName: "b",
|
subComponentName: "b",
|
||||||
globalAccountName: "c",
|
globalAccountName: "c",
|
||||||
databaseName: "d",
|
databaseName: "d",
|
||||||
@@ -174,27 +166,5 @@ describe("AppStatePersistenceUtility", () => {
|
|||||||
expect(key).toContain(storePath.databaseName);
|
expect(key).toContain(storePath.databaseName);
|
||||||
expect(key).toContain(storePath.containerName);
|
expect(key).toContain(storePath.containerName);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle components that include special characters", () => {
|
|
||||||
const storePath = {
|
|
||||||
componentName: AppStateComponentNames.DocumentsTab,
|
|
||||||
subComponentName: 'd"e"f',
|
|
||||||
globalAccountName: "g:hi{j",
|
|
||||||
databaseName: "a/b/c",
|
|
||||||
containerName: "https://blahblah.document.azure.com:443/",
|
|
||||||
};
|
|
||||||
const key = createKeyFromPath(storePath);
|
|
||||||
const segments = key.split(PATH_SEPARATOR);
|
|
||||||
expect(segments.length).toEqual(6); // There should be 5 segments
|
|
||||||
expect(segments[0]).toBe("");
|
|
||||||
|
|
||||||
const expectSubstringsInValue = (value: string, subStrings: string[]): boolean =>
|
|
||||||
subStrings.every((subString) => value.includes(subString));
|
|
||||||
|
|
||||||
expect(expectSubstringsInValue(segments[2], ["d", "e", "f"])).toBe(true);
|
|
||||||
expect(expectSubstringsInValue(segments[3], ["g", "hi", "j"])).toBe(true);
|
|
||||||
expect(expectSubstringsInValue(segments[4], ["a", "b", "c"])).toBe(true);
|
|
||||||
expect(expectSubstringsInValue(segments[5], ["https", "blahblah", "document", "com", "443"])).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
|
||||||
// The component name whose state is being saved. Component name must not include special characters.
|
// The component name whose state is being saved. Component name must not include special characters.
|
||||||
export enum AppStateComponentNames {
|
export type ComponentName = "DocumentsTab";
|
||||||
DocumentsTab = "DocumentsTab",
|
|
||||||
MostRecentActivity = "MostRecentActivity",
|
|
||||||
QueryCopilot = "QueryCopilot",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PATH_SEPARATOR = "/"; // export for testing purposes
|
|
||||||
const SCHEMA_VERSION = 1;
|
const SCHEMA_VERSION = 1;
|
||||||
|
|
||||||
// Export for testing purposes
|
// Export for testing purposes
|
||||||
@@ -19,9 +14,8 @@ export interface StateData {
|
|||||||
data: unknown;
|
data: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export for testing purposes
|
type StorePath = {
|
||||||
export type StorePath = {
|
componentName: string;
|
||||||
componentName: AppStateComponentNames;
|
|
||||||
subComponentName?: string;
|
subComponentName?: string;
|
||||||
globalAccountName?: string;
|
globalAccountName?: string;
|
||||||
databaseName?: string;
|
databaseName?: string;
|
||||||
@@ -35,7 +29,6 @@ export const loadState = (path: StorePath): unknown => {
|
|||||||
const key = createKeyFromPath(path);
|
const key = createKeyFromPath(path);
|
||||||
return appState[key]?.data;
|
return appState[key]?.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveState = (path: StorePath, state: unknown): void => {
|
export const saveState = (path: StorePath, state: unknown): void => {
|
||||||
// Retrieve state object
|
// Retrieve state object
|
||||||
const appState =
|
const appState =
|
||||||
@@ -67,10 +60,6 @@ export const deleteState = (path: StorePath): void => {
|
|||||||
LocalStorageUtility.setEntryObject(StorageKey.AppState, appState);
|
LocalStorageUtility.setEntryObject(StorageKey.AppState, appState);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasState = (path: StorePath): boolean => {
|
|
||||||
return loadState(path) !== undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is for high-frequency state changes
|
// This is for high-frequency state changes
|
||||||
let timeoutId: NodeJS.Timeout | undefined;
|
let timeoutId: NodeJS.Timeout | undefined;
|
||||||
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
|
export const saveStateDebounced = (path: StorePath, state: unknown, debounceDelayMs = 1000): void => {
|
||||||
@@ -98,10 +87,16 @@ const orderedPathSegments: (keyof StorePath)[] = [
|
|||||||
* @param path
|
* @param path
|
||||||
*/
|
*/
|
||||||
export const createKeyFromPath = (path: StorePath): string => {
|
export const createKeyFromPath = (path: StorePath): string => {
|
||||||
let key = `${PATH_SEPARATOR}${encodeURIComponent(path.componentName)}`; // ComponentName is always there
|
if (path.componentName.includes("/")) {
|
||||||
|
throw new Error(`Invalid component name: ${path.componentName}`);
|
||||||
|
}
|
||||||
|
let key = `/${path.componentName}`; // ComponentName is always there
|
||||||
orderedPathSegments.forEach((segment) => {
|
orderedPathSegments.forEach((segment) => {
|
||||||
const segmentValue = path[segment as keyof StorePath];
|
const segmentValue = path[segment as keyof StorePath];
|
||||||
key += `${PATH_SEPARATOR}${segmentValue !== undefined ? encodeURIComponent(segmentValue) : ""}`;
|
if (segmentValue.includes("/")) {
|
||||||
|
throw new Error(`Invalid setting path segment: ${segment}`);
|
||||||
|
}
|
||||||
|
key += `/${segmentValue !== undefined ? segmentValue : ""}`;
|
||||||
});
|
});
|
||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,12 +24,11 @@ export enum StorageKey {
|
|||||||
MaxDegreeOfParellism,
|
MaxDegreeOfParellism,
|
||||||
IsGraphAutoVizDisabled,
|
IsGraphAutoVizDisabled,
|
||||||
TenantId,
|
TenantId,
|
||||||
MostRecentActivity, // deprecated
|
MostRecentActivity,
|
||||||
SetPartitionKeyUndefined,
|
SetPartitionKeyUndefined,
|
||||||
GalleryCalloutDismissed,
|
GalleryCalloutDismissed,
|
||||||
VisitedAccounts,
|
VisitedAccounts,
|
||||||
PriorityLevel,
|
PriorityLevel,
|
||||||
DocumentsTabPrefs,
|
|
||||||
DefaultQueryResultsView,
|
DefaultQueryResultsView,
|
||||||
AppState,
|
AppState,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,11 +192,6 @@ export function useNewPortalBackendEndpoint(backendApi: string): boolean {
|
|||||||
PortalBackendEndpoints.Fairfax,
|
PortalBackendEndpoints.Fairfax,
|
||||||
PortalBackendEndpoints.Mooncake,
|
PortalBackendEndpoints.Mooncake,
|
||||||
],
|
],
|
||||||
[BackendApi.SampleData]: [
|
|
||||||
PortalBackendEndpoints.Development,
|
|
||||||
PortalBackendEndpoints.Mpac,
|
|
||||||
PortalBackendEndpoints.Prod,
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
||||||
|
|||||||
@@ -4,28 +4,7 @@ import * as sinon from "sinon";
|
|||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import * as QueryUtils from "./QueryUtils";
|
import * as QueryUtils from "./QueryUtils";
|
||||||
import { defaultQueryFields, extractPartitionKeyValues, getValueForPath } from "./QueryUtils";
|
import { extractPartitionKeyValues } from "./QueryUtils";
|
||||||
|
|
||||||
const documentContent = {
|
|
||||||
"Volcano Name": "Adams",
|
|
||||||
Country: "United States",
|
|
||||||
Region: "US-Washington",
|
|
||||||
Location: {
|
|
||||||
type: "Point",
|
|
||||||
coordinates: [-121.49, 46.206],
|
|
||||||
},
|
|
||||||
Elevation: 3742,
|
|
||||||
Type: "Stratovolcano",
|
|
||||||
Category: "",
|
|
||||||
Status: "Tephrochronology",
|
|
||||||
"Last Known Eruption": "Last known eruption from A.D. 1-1499, inclusive",
|
|
||||||
id: "9e3c494e-8367-3f50-1f56-8c6fcb961363",
|
|
||||||
_rid: "xzo0AJRYUxUFAAAAAAAAAA==",
|
|
||||||
_self: "dbs/xzo0AA==/colls/xzo0AJRYUxU=/docs/xzo0AJRYUxUFAAAAAAAAAA==/",
|
|
||||||
_etag: '"ce00fa43-0000-0100-0000-652840440000"',
|
|
||||||
_attachments: "attachments/",
|
|
||||||
_ts: 1697136708,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("Query Utils", () => {
|
describe("Query Utils", () => {
|
||||||
const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => {
|
const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => {
|
||||||
@@ -75,20 +54,6 @@ describe("Query Utils", () => {
|
|||||||
|
|
||||||
expect(partitionProjection).toContain('c["\\\\\\"a\\\\\\""]');
|
expect(partitionProjection).toContain('c["\\\\\\"a\\\\\\""]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should always include the default fields", () => {
|
|
||||||
const query: string = QueryUtils.buildDocumentsQuery("", [], generatePartitionKeyForPath("/a"), []);
|
|
||||||
|
|
||||||
defaultQueryFields.forEach((field) => {
|
|
||||||
expect(query).toContain(`c.${field}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should always include the default fields even if they are themselves partition key fields", () => {
|
|
||||||
const query: string = QueryUtils.buildDocumentsQuery("", ["id"], generatePartitionKeyForPath("/id"), ["id"]);
|
|
||||||
|
|
||||||
expect(query).toContain("c.id");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("queryPagesUntilContentPresent()", () => {
|
describe("queryPagesUntilContentPresent()", () => {
|
||||||
@@ -132,30 +97,28 @@ describe("Query Utils", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getValueForPath", () => {
|
|
||||||
it("should return the correct value for a simple path", () => {
|
|
||||||
const pathSegments = ["Volcano Name"];
|
|
||||||
expect(getValueForPath(documentContent, pathSegments)).toBe("Adams");
|
|
||||||
});
|
|
||||||
it("should return the correct value for a nested path", () => {
|
|
||||||
const pathSegments = ["Location", "coordinates"];
|
|
||||||
expect(getValueForPath(documentContent, pathSegments)).toEqual([-121.49, 46.206]);
|
|
||||||
});
|
|
||||||
it("should return undefined for a non-existing path", () => {
|
|
||||||
const pathSegments = ["NonExistent", "Path"];
|
|
||||||
expect(getValueForPath(documentContent, pathSegments)).toBeUndefined();
|
|
||||||
});
|
|
||||||
it("should return undefined for an invalid path", () => {
|
|
||||||
const pathSegments = ["Location", "InvalidKey"];
|
|
||||||
expect(getValueForPath(documentContent, pathSegments)).toBeUndefined();
|
|
||||||
});
|
|
||||||
it("should return the root object if pathSegments is empty", () => {
|
|
||||||
const pathSegments: string[] = [];
|
|
||||||
expect(getValueForPath(documentContent, pathSegments)).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("extractPartitionKey", () => {
|
describe("extractPartitionKey", () => {
|
||||||
|
const documentContent = {
|
||||||
|
"Volcano Name": "Adams",
|
||||||
|
Country: "United States",
|
||||||
|
Region: "US-Washington",
|
||||||
|
Location: {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: [-121.49, 46.206],
|
||||||
|
},
|
||||||
|
Elevation: 3742,
|
||||||
|
Type: "Stratovolcano",
|
||||||
|
Category: "",
|
||||||
|
Status: "Tephrochronology",
|
||||||
|
"Last Known Eruption": "Last known eruption from A.D. 1-1499, inclusive",
|
||||||
|
id: "9e3c494e-8367-3f50-1f56-8c6fcb961363",
|
||||||
|
_rid: "xzo0AJRYUxUFAAAAAAAAAA==",
|
||||||
|
_self: "dbs/xzo0AA==/colls/xzo0AJRYUxU=/docs/xzo0AJRYUxUFAAAAAAAAAA==/",
|
||||||
|
_etag: '"ce00fa43-0000-0100-0000-652840440000"',
|
||||||
|
_attachments: "attachments/",
|
||||||
|
_ts: 1697136708,
|
||||||
|
};
|
||||||
|
|
||||||
it("should extract single partition key value", () => {
|
it("should extract single partition key value", () => {
|
||||||
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
|
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
|
||||||
kind: PartitionKeyKind.Hash,
|
kind: PartitionKeyKind.Hash,
|
||||||
@@ -212,18 +175,5 @@ describe("Query Utils", () => {
|
|||||||
);
|
);
|
||||||
expect(partitionKeyValues.length).toBe(0);
|
expect(partitionKeyValues.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should extract all partition key values for hierarchical and nested partition keys", () => {
|
|
||||||
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
|
|
||||||
kind: PartitionKeyKind.MultiHash,
|
|
||||||
paths: ["/Country", "/Location/type"],
|
|
||||||
};
|
|
||||||
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
|
||||||
documentContent,
|
|
||||||
mixedPartitionKeyDefinition,
|
|
||||||
);
|
|
||||||
expect(partitionKeyValues.length).toBe(2);
|
|
||||||
expect(partitionKeyValues).toEqual(["United States", "Point"]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,29 +2,18 @@ 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) => {
|
|
||||||
if (!partitionKeyProperties.includes(prop)) {
|
|
||||||
fieldSet.add(prop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const objectListSpec = [...fieldSet].map((prop) => `c.${prop}`).join(",");
|
|
||||||
let query =
|
let query =
|
||||||
partitionKeyProperties && partitionKeyProperties.length > 0
|
partitionKeyProperties && partitionKeyProperties.length > 0
|
||||||
? `select ${objectListSpec}, [${buildDocumentsQueryPartitionProjections(
|
? `select c.id, c._self, c._rid, c._ts, [${buildDocumentsQueryPartitionProjections(
|
||||||
"c",
|
"c",
|
||||||
partitionKey,
|
partitionKey,
|
||||||
)}] as _partitionKeyValue from c`
|
)}] as _partitionKeyValue from c`
|
||||||
: `select ${objectListSpec} from c`;
|
: `select c.id, c._self, c._rid, c._ts from c`;
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
query += " " + filter;
|
query += " " + filter;
|
||||||
@@ -95,24 +84,6 @@ export const queryPagesUntilContentPresent = async (
|
|||||||
return await doRequest(firstItemIndex);
|
return await doRequest(firstItemIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
export const getValueForPath = (content: any, pathSegments: string[]): any => {
|
|
||||||
if (pathSegments.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentValue = content;
|
|
||||||
|
|
||||||
for (const segment of pathSegments) {
|
|
||||||
if (!currentValue || currentValue[segment] === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
currentValue = currentValue[segment];
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
export const extractPartitionKeyValues = (
|
export const extractPartitionKeyValues = (
|
||||||
documentContent: any,
|
documentContent: any,
|
||||||
@@ -123,15 +94,11 @@ export const extractPartitionKeyValues = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const partitionKeyValues: PartitionKey[] = [];
|
const partitionKeyValues: PartitionKey[] = [];
|
||||||
|
|
||||||
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
|
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
|
||||||
const pathSegments: string[] = partitionKeyPath.substring(1).split("/");
|
const partitionKeyPathWithoutSlash: string = partitionKeyPath.substring(1);
|
||||||
const value = getValueForPath(documentContent, pathSegments);
|
if (documentContent[partitionKeyPathWithoutSlash] !== undefined) {
|
||||||
|
partitionKeyValues.push(documentContent[partitionKeyPathWithoutSlash]);
|
||||||
if (value !== undefined) {
|
|
||||||
partitionKeyValues.push(value);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return partitionKeyValues;
|
return partitionKeyValues;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
|||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
|
||||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -761,17 +760,11 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let url: string;
|
const sampleDatabaseEndpoint = useQueryCopilot.getState().copilotUserDBEnabled
|
||||||
if (useNewPortalBackendEndpoint(Constants.BackendApi.SampleData)) {
|
? `/api/tokens/sampledataconnection/v2`
|
||||||
url = createUri(configContext.PORTAL_BACKEND_ENDPOINT, "/api/sampledata");
|
: `/api/tokens/sampledataconnection`;
|
||||||
} else {
|
|
||||||
const sampleDatabaseEndpoint = useQueryCopilot.getState().copilotUserDBEnabled
|
|
||||||
? `/api/tokens/sampledataconnection/v2`
|
|
||||||
: `/api/tokens/sampledataconnection`;
|
|
||||||
|
|
||||||
url = createUri(`${configContext.BACKEND_ENDPOINT}`, sampleDatabaseEndpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const url = createUri(`${configContext.BACKEND_ENDPOINT}`, sampleDatabaseEndpoint);
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user