mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-08 12:07:06 +00:00
Compare commits
26 Commits
users/lang
...
ashleyst/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf8359c548 | ||
|
|
b03925abab | ||
|
|
53d3413c62 | ||
|
|
7128133874 | ||
|
|
053dc9d76b | ||
|
|
23b2e59560 | ||
|
|
869d81dfbc | ||
|
|
42a1c6c319 | ||
|
|
9f1cc4cd5c | ||
|
|
78154bd976 | ||
|
|
91649d2f52 | ||
|
|
d7647b2ecf | ||
|
|
2c7e788358 | ||
|
|
fdbbbd7378 | ||
|
|
82bdeff158 | ||
|
|
825a5d5257 | ||
|
|
d75553a94d | ||
|
|
50c47a82d6 | ||
|
|
2c2f0c8d7b | ||
|
|
cfc8196c4b | ||
|
|
87024f4bf4 | ||
|
|
fe9730206e | ||
|
|
7e95f5d8c8 | ||
|
|
1be221e106 | ||
|
|
8e7a3db67e | ||
|
|
07c0ead523 |
16
.devcontainer/Dockerfile
Normal file
16
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
32
.devcontainer/devcontainer.json
Normal file
32
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 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"
|
||||||
|
}
|
||||||
4
.devcontainer/oncreate
Executable file
4
.devcontainer/oncreate
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Install packages once, to prime the node_modules directory.
|
||||||
|
npm ci
|
||||||
@@ -2618,6 +2618,7 @@ a:link {
|
|||||||
.tabPanesContainer {
|
.tabPanesContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ 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 {
|
||||||
@@ -292,6 +293,7 @@ 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;
|
||||||
@@ -503,7 +505,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,3 +1,5 @@
|
|||||||
|
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", () => {
|
||||||
@@ -11,4 +13,18 @@ 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,6 +1,29 @@
|
|||||||
|
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,21 +723,19 @@ 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> {
|
||||||
@@ -747,6 +745,14 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
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[];
|
|
||||||
};
|
|
||||||
94
src/Common/QueryError.test.ts
Normal file
94
src/Common/QueryError.test.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
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,13 +97,44 @@ 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(
|
||||||
public message: string,
|
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.
|
||||||
@@ -135,7 +166,7 @@ export default class QueryError {
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error as string | Error);
|
const errorMessage = error as string;
|
||||||
|
|
||||||
// Map some well known messages to richer errors
|
// Map some well known messages to richer errors
|
||||||
const knownError = knownErrors[errorMessage];
|
const knownError = knownErrors[errorMessage];
|
||||||
@@ -160,7 +191,9 @@ export default class QueryError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const severity =
|
const severity =
|
||||||
"severity" in error && typeof error.severity === "string" ? (error.severity as QueryErrorSeverity) : undefined;
|
"severity" in error && typeof error.severity === "string"
|
||||||
|
? (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 })
|
||||||
@@ -173,16 +206,15 @@ 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 {
|
||||||
if (typeof error === "object" && "message" in error) {
|
let message: string | undefined;
|
||||||
error = error.message;
|
if (typeof error === "object" && "message" in error && typeof error.message === "string") {
|
||||||
}
|
message = error.message;
|
||||||
|
} else {
|
||||||
if (typeof error !== "string") {
|
// Unsupported error format.
|
||||||
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.
|
||||||
@@ -196,13 +228,16 @@ export default class QueryError {
|
|||||||
try {
|
try {
|
||||||
parsed = JSON.parse(message);
|
parsed = JSON.parse(message);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Not a query error.
|
// The message doesn't contain a nested error.
|
||||||
return null;
|
return [QueryError.read(error, locationResolver)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof parsed === "object" && "errors" in parsed && Array.isArray(parsed.errors)) {
|
if (typeof parsed === "object") {
|
||||||
|
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,11 +3,12 @@ import * as React from "react";
|
|||||||
|
|
||||||
export interface TooltipProps {
|
export interface TooltipProps {
|
||||||
children: string;
|
children: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }: TooltipProps) => {
|
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children, className }: TooltipProps) => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className={className}>
|
||||||
<TooltipHost content={children}>
|
<TooltipHost content={children}>
|
||||||
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
|
|||||||
@@ -26,14 +26,23 @@ 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 ids that were successfully deleted
|
* @returns array of results and status codes
|
||||||
*/
|
*/
|
||||||
export const deleteDocuments = async (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> => {
|
export const deleteDocuments = async (
|
||||||
const nbDocuments = documentIds.length;
|
collection: CollectionBase,
|
||||||
|
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());
|
||||||
@@ -56,18 +65,17 @@ export const deleteDocuments = async (collection: CollectionBase, documentIds: D
|
|||||||
operationType: BulkOperationType.Delete,
|
operationType: BulkOperationType.Delete,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const promise = v2Container.items.bulk(operations).then((bulkResult) => {
|
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
|
||||||
return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204);
|
return bulkResults.map((bulkResult, index) => {
|
||||||
|
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,14 +49,12 @@ 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;
|
||||||
@@ -117,12 +115,10 @@ 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,7 +98,6 @@ 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 {
|
||||||
@@ -191,8 +190,6 @@ 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,23 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
@@ -31,7 +22,7 @@ export interface CommandButtonComponentProps {
|
|||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
*/
|
*/
|
||||||
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent, container: Explorer) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
@@ -120,157 +111,3 @@ 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) => void;
|
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => 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): void =>
|
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps): void =>
|
||||||
get().openDialog({
|
get().openDialog({
|
||||||
isModal: true,
|
isModal: true,
|
||||||
title,
|
title,
|
||||||
@@ -94,6 +94,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
|||||||
get().closeDialog();
|
get().closeDialog();
|
||||||
},
|
},
|
||||||
onSecondaryButtonClick: undefined,
|
onSecondaryButtonClick: undefined,
|
||||||
|
linkProps,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
79
src/Explorer/Controls/ProgressModalDialog.tsx
Normal file
79
src/Explorer/Controls/ProgressModalDialog.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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,7 +134,6 @@ 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,6 +7,7 @@ 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";
|
||||||
@@ -32,7 +33,6 @@ 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,7 +130,6 @@ export interface SettingsComponentState {
|
|||||||
conflictResolutionPolicyProcedureBaseline: string;
|
conflictResolutionPolicyProcedureBaseline: string;
|
||||||
isConflictResolutionDirty: boolean;
|
isConflictResolutionDirty: boolean;
|
||||||
|
|
||||||
initialNotification: DataModels.Notification;
|
|
||||||
selectedTab: SettingsV2TabTypes;
|
selectedTab: SettingsV2TabTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +228,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
conflictResolutionPolicyProcedureBaseline: undefined,
|
conflictResolutionPolicyProcedureBaseline: undefined,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
|
|
||||||
initialNotification: undefined,
|
|
||||||
selectedTab: SettingsV2TabTypes.ScaleTab,
|
selectedTab: SettingsV2TabTypes.ScaleTab,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1052,7 +1050,6 @@ 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,18 +1,10 @@
|
|||||||
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,
|
||||||
@@ -36,39 +28,8 @@ 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,7 +10,6 @@ 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,
|
||||||
@@ -34,7 +33,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,10 +100,6 @@ 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(
|
||||||
@@ -120,26 +114,6 @@ 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}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
// 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,7 +44,6 @@ 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);
|
||||||
|
|||||||
@@ -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" && !isBranch && node.onClick) {
|
if (data.type === "Click" && 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?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isBranch, node, setIsLoading],
|
[node, setIsLoading],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMenuOpenChange = useCallback(
|
const onMenuOpenChange = useCallback(
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
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";
|
||||||
@@ -46,7 +48,6 @@ 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";
|
||||||
@@ -1178,7 +1179,11 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async configureCopilot(): Promise<void> {
|
public async configureCopilot(): Promise<void> {
|
||||||
if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
|
if (
|
||||||
|
userContext.apiType !== "SQL" ||
|
||||||
|
!userContext.subscriptionId ||
|
||||||
|
![Environment.Development, Environment.Mpac, Environment.Prod].includes(getEnvironment())
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const copilotEnabledPromise = getCopilotEnabled();
|
const copilotEnabledPromise = getCopilotEnabled();
|
||||||
|
|||||||
@@ -4,83 +4,51 @@
|
|||||||
* 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 buttons = useCommandBar((state) => state.contextButtons);
|
const contextButtons = 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();
|
||||||
|
|
||||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor, container);
|
||||||
const buttons =
|
if (contextButtons?.length > 0) {
|
||||||
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(contextButtons, backgroundColor);
|
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(
|
||||||
|
contextButtons || [],
|
||||||
|
backgroundColor,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
if (uiFabricTabsButtons.length > 0) {
|
if (uiFabricTabsButtons.length > 0) {
|
||||||
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
const uiFabricPlatformButtons = CommandBarUtil.convertButton(platformButtons || [], backgroundColor, container);
|
||||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricPlatformButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
|
|
||||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
|
|
||||||
@@ -88,7 +56,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
|
||||||
) {
|
) {
|
||||||
uiFabricControlButtons.unshift(
|
uiFabricPlatformButtons.unshift(
|
||||||
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
|
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -107,8 +75,8 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
const allButtons = staticButtons.concat(contextButtons).concat(platformButtons);
|
||||||
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons, container);
|
||||||
setKeyboardHandlers(keyboardHandlers);
|
setKeyboardHandlers(keyboardHandlers);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -116,7 +84,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={uiFabricControlButtons}
|
farItems={uiFabricPlatformButtons}
|
||||||
styles={rootStyle}
|
styles={rootStyle}
|
||||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,15 +3,12 @@ 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", () => {
|
||||||
@@ -19,7 +16,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockExplorer = {} as Explorer;
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -30,7 +26,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Button should be visible", () => {
|
it("Button should be visible", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -46,7 +42,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -62,7 +58,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const enableAzureSynapseLinkBtn = buttons.find(
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
);
|
);
|
||||||
@@ -75,7 +71,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockExplorer = {} as Explorer;
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
properties: {
|
properties: {
|
||||||
@@ -108,7 +103,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
},
|
},
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -118,13 +113,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
portalEnv: "mooncake",
|
portalEnv: "mooncake",
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(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(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState);
|
||||||
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||||
expect(openCassandraShellBtn).toBeUndefined();
|
expect(openCassandraShellBtn).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -134,12 +129,8 @@ 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(mockExplorer);
|
const buttons = CommandBarComponentButtonFactory.createPostgreButtons();
|
||||||
const openPostgresShellButton = buttons.find(
|
const openPostgresShellButton = buttons.find(
|
||||||
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
|
(button) => button.commandButtonLabel === openPostgresShellButtonLabel,
|
||||||
);
|
);
|
||||||
@@ -147,7 +138,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("creates vCore Mongo shell button", () => {
|
it("creates vCore Mongo shell button", () => {
|
||||||
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(mockExplorer);
|
const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons();
|
||||||
const openVCoreMongoShellButton = buttons.find(
|
const openVCoreMongoShellButton = buttons.find(
|
||||||
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
|
(button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel,
|
||||||
);
|
);
|
||||||
@@ -162,8 +153,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
const selectedNodeState = useSelectedNode.getState();
|
const selectedNodeState = useSelectedNode.getState();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockExplorer = {} as Explorer;
|
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
authType: AuthType.ResourceToken,
|
authType: AuthType.ResourceToken,
|
||||||
});
|
});
|
||||||
@@ -175,7 +164,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
kind: "DocumentDB",
|
kind: "DocumentDB",
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(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,7 +21,6 @@ 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";
|
||||||
@@ -32,19 +31,20 @@ import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
|
|||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
export function createStaticCommandBarButtons(
|
export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
|
||||||
container: Explorer,
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
selectedNodeState: SelectedNodeState,
|
return userContext.apiType === "Postgres" ? createPostgreButtons() : createVCoreMongoButtons();
|
||||||
): CommandButtonComponentProps[] {
|
}
|
||||||
|
|
||||||
if (userContext.authType === AuthType.ResourceToken) {
|
if (userContext.authType === AuthType.ResourceToken) {
|
||||||
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
return createStaticCommandBarButtonsForResourceToken(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) {
|
if (buttons.length > 0 && !buttons[buttons.length - 1].isDivider) {
|
||||||
buttons.push(createDivider());
|
buttons.push(createDivider());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ export function createStaticCommandBarButtons(
|
|||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables" &&
|
||||||
userContext.apiType !== "Cassandra"
|
userContext.apiType !== "Cassandra"
|
||||||
) {
|
) {
|
||||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
const addSynapseLink = createOpenSynapseLinkDialogButton();
|
||||||
if (addSynapseLink) {
|
if (addSynapseLink) {
|
||||||
addDivider();
|
addDivider();
|
||||||
buttons.push(addSynapseLink);
|
buttons.push(addSynapseLink);
|
||||||
@@ -67,9 +67,9 @@ export function createStaticCommandBarButtons(
|
|||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buttonProps = createLoginForEntraIDButton(container);
|
const buttonProps = createLoginForEntraIDButton();
|
||||||
setLoginButtonProps(buttonProps);
|
setLoginButtonProps(buttonProps);
|
||||||
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
|
}, [dataPlaneRbacEnabled, aadTokenUpdated]);
|
||||||
|
|
||||||
if (loginButtonProps) {
|
if (loginButtonProps) {
|
||||||
addDivider();
|
addDivider();
|
||||||
@@ -87,8 +87,8 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
||||||
const openQueryBtn = createOpenQueryButton(container);
|
const openQueryBtn = createOpenQueryButton();
|
||||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
|
||||||
buttons.push(openQueryBtn);
|
buttons.push(openQueryBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +103,7 @@ export function createStaticCommandBarButtons(
|
|||||||
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:
|
||||||
@@ -115,21 +116,12 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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: () => {
|
onCommandClick: (_, container) => {
|
||||||
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);
|
||||||
@@ -141,6 +133,7 @@ export function createContextCommandBarButtons(
|
|||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
};
|
};
|
||||||
|
addDivider();
|
||||||
buttons.push(newMongoShellBtn);
|
buttons.push(newMongoShellBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,25 +146,27 @@ export function createContextCommandBarButtons(
|
|||||||
const newCassandraShellButton: CommandButtonComponentProps = {
|
const newCassandraShellButton: CommandButtonComponentProps = {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: (_, container) => {
|
||||||
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 createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createPlatformButtons(): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] = [
|
const buttons: CommandButtonComponentProps[] = [
|
||||||
{
|
{
|
||||||
iconSrc: SettingsIcon,
|
iconSrc: SettingsIcon,
|
||||||
iconAlt: "Settings",
|
iconAlt: "Settings",
|
||||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
onCommandClick: (_, container) =>
|
||||||
|
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: "Settings",
|
ariaLabel: "Settings",
|
||||||
tooltipText: "Settings",
|
tooltipText: "Settings",
|
||||||
@@ -207,7 +202,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
|||||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||||
iconSrc: FeedbackIcon,
|
iconSrc: FeedbackIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => container.openCESCVAFeedbackBlade(),
|
onCommandClick: (_, container) => container.openCESCVAFeedbackBlade(),
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
tooltipText: label,
|
tooltipText: label,
|
||||||
@@ -239,7 +234,7 @@ function areScriptsSupported(): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
|
function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps {
|
||||||
if (configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -257,7 +252,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
|||||||
return {
|
return {
|
||||||
iconSrc: SynapseIcon,
|
iconSrc: SynapseIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
onCommandClick: (_, container) => container.openEnableSynapseLinkDialog(),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled:
|
disabled:
|
||||||
@@ -266,12 +261,12 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps {
|
function createLoginForEntraIDButton(): CommandButtonComponentProps {
|
||||||
if (configContext.platform !== Platform.Portal) {
|
if (configContext.platform !== Platform.Portal) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCommandClick = async () => {
|
const handleCommandClick: CommandButtonComponentProps["onCommandClick"] = async (_, container) => {
|
||||||
await container.openLoginForEntraIDPopUp();
|
await container.openLoginForEntraIDPopUp();
|
||||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||||
};
|
};
|
||||||
@@ -398,13 +393,14 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
|||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
|
function createOpenQueryButton(): 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: () =>
|
onCommandClick: (_, container) =>
|
||||||
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,
|
||||||
@@ -427,10 +423,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOpenTerminalButtonByKind(
|
function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind): CommandButtonComponentProps {
|
||||||
container: Explorer,
|
|
||||||
terminalKind: ViewModels.TerminalKind,
|
|
||||||
): CommandButtonComponentProps {
|
|
||||||
const terminalFriendlyName = (): string => {
|
const terminalFriendlyName = (): string => {
|
||||||
switch (terminalKind) {
|
switch (terminalKind) {
|
||||||
case ViewModels.TerminalKind.Cassandra:
|
case ViewModels.TerminalKind.Cassandra:
|
||||||
@@ -453,7 +446,7 @@ function createOpenTerminalButtonByKind(
|
|||||||
return {
|
return {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: (_, container) => {
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
container.openNotebookTerminal(terminalKind);
|
container.openNotebookTerminal(terminalKind);
|
||||||
}
|
}
|
||||||
@@ -467,11 +460,10 @@ function createOpenTerminalButtonByKind(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createStaticCommandBarButtonsForResourceToken(
|
function createStaticCommandBarButtonsForResourceToken(
|
||||||
container: Explorer,
|
|
||||||
selectedNodeState: SelectedNodeState,
|
selectedNodeState: SelectedNodeState,
|
||||||
): CommandButtonComponentProps[] {
|
): CommandButtonComponentProps[] {
|
||||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||||
const openQueryBtn = createOpenQueryButton(container);
|
const openQueryBtn = createOpenQueryButton();
|
||||||
|
|
||||||
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
|
||||||
const isResourceTokenCollectionNodeSelected: boolean =
|
const isResourceTokenCollectionNodeSelected: boolean =
|
||||||
@@ -484,20 +476,20 @@ function createStaticCommandBarButtonsForResourceToken(
|
|||||||
|
|
||||||
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
|
||||||
if (!openQueryBtn.disabled) {
|
if (!openQueryBtn.disabled) {
|
||||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [newSqlQueryBtn, openQueryBtn];
|
return [newSqlQueryBtn, openQueryBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createPostgreButtons(): CommandButtonComponentProps[] {
|
||||||
const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres);
|
const openPostgreShellBtn = createOpenTerminalButtonByKind(ViewModels.TerminalKind.Postgres);
|
||||||
|
|
||||||
return [openPostgreShellBtn];
|
return [openPostgreShellBtn];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createVCoreMongoButtons(): CommandButtonComponentProps[] {
|
||||||
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
|
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(ViewModels.TerminalKind.VCoreMongo);
|
||||||
|
|
||||||
return [openVCoreMongoTerminalButton];
|
return [openVCoreMongoTerminalButton];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
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",
|
||||||
@@ -22,7 +24,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
const btn = createButton();
|
const btn = createButton();
|
||||||
const backgroundColor = "backgroundColor";
|
const backgroundColor = "backgroundColor";
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton([btn], backgroundColor);
|
const converteds = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer);
|
||||||
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);
|
||||||
@@ -46,7 +48,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
btn.children.push(child);
|
btn.children.push(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor");
|
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor", mockExplorer);
|
||||||
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);
|
||||||
@@ -62,7 +64,7 @@ describe("CommandBarUtil tests", () => {
|
|||||||
btns.push(createButton());
|
btns.push(createButton());
|
||||||
}
|
}
|
||||||
|
|
||||||
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor");
|
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor", mockExplorer);
|
||||||
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);
|
||||||
@@ -74,10 +76,10 @@ describe("CommandBarUtil tests", () => {
|
|||||||
const backgroundColor = "backgroundColor";
|
const backgroundColor = "backgroundColor";
|
||||||
|
|
||||||
btn.commandButtonLabel = undefined;
|
btn.commandButtonLabel = undefined;
|
||||||
let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
let converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
|
||||||
expect(converted.text).toEqual(btn.tooltipText);
|
expect(converted.text).toEqual(btn.tooltipText);
|
||||||
|
|
||||||
converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0];
|
||||||
delete btn.commandButtonLabel;
|
delete btn.commandButtonLabel;
|
||||||
expect(converted.text).toEqual(btn.tooltipText);
|
expect(converted.text).toEqual(btn.tooltipText);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ 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 = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
export const convertButton = (
|
||||||
|
btns: CommandButtonComponentProps[],
|
||||||
|
backgroundColor: string,
|
||||||
|
container: Explorer,
|
||||||
|
): ICommandBarItemProps[] => {
|
||||||
const buttonHeightPx =
|
const buttonHeightPx =
|
||||||
configContext.platform == Platform.Fabric
|
configContext.platform == Platform.Fabric
|
||||||
? StyleConstants.FabricCommandBarButtonHeight
|
? StyleConstants.FabricCommandBarButtonHeight
|
||||||
@@ -54,15 +58,14 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
iconProps: {
|
iconProps: {
|
||||||
style: {
|
style: {
|
||||||
width: StyleConstants.CommandBarIconWidth, // 16
|
width: StyleConstants.CommandBarIconWidth, // 16
|
||||||
alignSelf: btn.iconName ? "baseline" : undefined,
|
alignSelf: 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);
|
btn.onCommandClick(ev, container);
|
||||||
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;
|
||||||
@@ -135,7 +138,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
result.split = true;
|
result.split = true;
|
||||||
|
|
||||||
result.subMenuProps = {
|
result.subMenuProps = {
|
||||||
items: convertButton(btn.children, backgroundColor),
|
items: convertButton(btn.children, backgroundColor, container),
|
||||||
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.
|
||||||
@@ -186,7 +189,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
option?: IDropdownOption,
|
option?: IDropdownOption,
|
||||||
index?: number,
|
index?: number,
|
||||||
): void => {
|
): void => {
|
||||||
btn.children[index].onCommandClick(event);
|
btn.children[index].onCommandClick(event, container);
|
||||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,14 +240,17 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType,
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
|
export function createKeyboardHandlers(
|
||||||
|
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);
|
button.onCommandClick(e, container);
|
||||||
|
|
||||||
// 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;
|
||||||
|
|||||||
16
src/Explorer/Menus/CommandBar/useCommandBar.ts
Normal file
16
src/Explorer/Menus/CommandBar/useCommandBar.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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 })),
|
||||||
|
}));
|
||||||
159
src/Explorer/Menus/CommandBarV2/CommandBarV2.tsx
Normal file
159
src/Explorer/Menus/CommandBarV2/CommandBarV2.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
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 accountId = "some account";
|
const accountName = "some account";
|
||||||
|
|
||||||
beforeEach(() => mostRecentActivity.clear(accountId));
|
beforeEach(() => clear(accountName));
|
||||||
|
|
||||||
it("Has no items at first", () => {
|
it("Has no items at first", () => {
|
||||||
expect(mostRecentActivity.getItems(accountId)).toStrictEqual([]);
|
expect(getItems(accountName)).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Can record collections being opened", () => {
|
it("Can record collections being opened", () => {
|
||||||
@@ -18,9 +18,9 @@ describe("MostRecentActivity", () => {
|
|||||||
databaseId,
|
databaseId,
|
||||||
};
|
};
|
||||||
|
|
||||||
mostRecentActivity.collectionWasOpened(accountId, collection);
|
collectionWasOpened(accountName, collection);
|
||||||
|
|
||||||
const activity = mostRecentActivity.getItems(accountId);
|
const activity = getItems(accountName);
|
||||||
expect(activity).toEqual([
|
expect(activity).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
collectionId,
|
collectionId,
|
||||||
@@ -29,58 +29,24 @@ describe("MostRecentActivity", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Can record notebooks being opened", () => {
|
it("Does not store duplicate entries", () => {
|
||||||
const name = "some notebook";
|
const collectionId = "some collection";
|
||||||
const path = "some path";
|
const databaseId = "some database";
|
||||||
const notebook = { name, path };
|
const collection = {
|
||||||
|
id: observable(collectionId),
|
||||||
|
databaseId,
|
||||||
|
};
|
||||||
|
|
||||||
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
|
collectionWasOpened(accountName, collection);
|
||||||
|
collectionWasOpened(accountName, collection);
|
||||||
|
|
||||||
const activity = mostRecentActivity.getItems(accountId);
|
const activity = getItems(accountName);
|
||||||
expect(activity).toEqual([expect.objectContaining(notebook)]);
|
expect(activity).toEqual([
|
||||||
});
|
expect.objectContaining({
|
||||||
|
type: Type.OpenCollection,
|
||||||
it("Filters out duplicates", () => {
|
collectionId,
|
||||||
const name = "some notebook";
|
databaseId,
|
||||||
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 { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } 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,158 +21,174 @@ export interface OpenCollectionItem {
|
|||||||
|
|
||||||
type Item = OpenNotebookItem | OpenCollectionItem;
|
type Item = OpenNotebookItem | OpenCollectionItem;
|
||||||
|
|
||||||
// Update schemaVersion if you are going to change this interface
|
const itemsMaxNumber: number = 5;
|
||||||
interface StoredData {
|
|
||||||
schemaVersion: string;
|
|
||||||
itemsMap: { [accountId: string]: Item[] }; // FIFO
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores most recent activity
|
* Migrate old data to new AppState
|
||||||
*/
|
*/
|
||||||
class MostRecentActivity {
|
const migrateOldData = () => {
|
||||||
private static readonly schemaVersion: string = "2";
|
|
||||||
private static itemsMaxNumber: number = 5;
|
|
||||||
private storedData: StoredData;
|
|
||||||
constructor() {
|
|
||||||
// Retrieve from local storage
|
|
||||||
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
||||||
|
const oldDataSchemaVersion: string = "2";
|
||||||
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
|
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
|
||||||
|
if (rawData) {
|
||||||
if (!rawData) {
|
const oldData = JSON.parse(rawData);
|
||||||
this.storedData = MostRecentActivity.createEmptyData();
|
if (oldData.schemaVersion === oldDataSchemaVersion) {
|
||||||
} else {
|
const itemsMap: Record<string, Item[]> = oldData.itemsMap;
|
||||||
try {
|
Object.keys(itemsMap).forEach((accountId: string) => {
|
||||||
this.storedData = JSON.parse(rawData);
|
const accountName = accountId.split("/").pop();
|
||||||
} catch (e) {
|
if (accountName) {
|
||||||
console.error("Unable to parse stored most recent activity. Use empty data:", rawData);
|
saveState(
|
||||||
this.storedData = MostRecentActivity.createEmptyData();
|
{
|
||||||
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
|
globalAccountName: accountName,
|
||||||
|
},
|
||||||
|
itemsMap[accountId].map((item) => {
|
||||||
|
if ((item.type as unknown as number) === 0) {
|
||||||
|
item.type = Type.OpenCollection;
|
||||||
|
} else if ((item.type as unknown as number) === 1) {
|
||||||
|
item.type = Type.OpenNotebook;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If version doesn't match or schema broke, nuke it!
|
// Remove old data
|
||||||
if (
|
|
||||||
!this.storedData.hasOwnProperty("schemaVersion") ||
|
|
||||||
this.storedData["schemaVersion"] !== MostRecentActivity.schemaVersion
|
|
||||||
) {
|
|
||||||
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
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) {
|
const addItem = (accountName: string, newItem: Item): void => {
|
||||||
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.
|
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
|
||||||
// if (!accountId) {
|
// if (!accountId) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
let items =
|
||||||
|
(loadState({
|
||||||
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
|
globalAccountName: accountName,
|
||||||
|
}) as Item[]) || [];
|
||||||
|
|
||||||
// Remove duplicate
|
// Remove duplicate
|
||||||
MostRecentActivity.removeDuplicate(newItem, this.storedData.itemsMap[accountId]);
|
items = removeDuplicate(newItem, items);
|
||||||
|
|
||||||
this.storedData.itemsMap[accountId] = this.storedData.itemsMap[accountId] || [];
|
items.unshift(newItem);
|
||||||
this.storedData.itemsMap[accountId].unshift(newItem);
|
items = cleanupItems(items, accountName);
|
||||||
this.cleanupItems(accountId);
|
saveState(
|
||||||
this.saveToLocalStorage();
|
{
|
||||||
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
|
globalAccountName: accountName,
|
||||||
|
},
|
||||||
|
items,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getItems = (accountName: string): Item[] => {
|
||||||
|
if (!accountName) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getItems(accountId: string): Item[] {
|
return (
|
||||||
return this.storedData.itemsMap[accountId] || [];
|
(loadState({
|
||||||
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
|
globalAccountName: accountName,
|
||||||
|
}) as Item[]) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const collectionWasOpened = (
|
||||||
|
accountName: string,
|
||||||
|
{ id, databaseId }: Pick<CollectionBase, "id" | "databaseId">,
|
||||||
|
) => {
|
||||||
|
if (accountName === undefined) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public collectionWasOpened(accountId: string, { id, databaseId }: Pick<CollectionBase, "id" | "databaseId">) {
|
|
||||||
const collectionId = id();
|
const collectionId = id();
|
||||||
this.addItem(accountId, {
|
addItem(accountName, {
|
||||||
type: Type.OpenCollection,
|
type: Type.OpenCollection,
|
||||||
databaseId,
|
databaseId,
|
||||||
collectionId,
|
collectionId,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
public notebookWasItemOpened(accountId: string, { name, path }: Pick<NotebookContentItem, "name" | "path">) {
|
export const clear = (accountName: string): void => {
|
||||||
this.addItem(accountId, {
|
if (!accountName) {
|
||||||
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;
|
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;
|
let index = -1;
|
||||||
for (let i = 0; i < itemsArray.length; i++) {
|
for (let i = 0; i < result.length; i++) {
|
||||||
const currentItem = itemsArray[i];
|
const currentItem = result[i];
|
||||||
if (JSON.stringify(currentItem) === JSON.stringify(item)) {
|
|
||||||
|
if (
|
||||||
|
JSON.stringify(sortObjectKeys(currentItem as unknown as Record<string, unknown>)) ===
|
||||||
|
JSON.stringify(sortObjectKeys(item as unknown as Record<string, unknown>))
|
||||||
|
) {
|
||||||
index = i;
|
index = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
itemsArray.splice(index, 1);
|
result.splice(index, 1);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove unknown types
|
* Remove unknown types
|
||||||
* Limit items to max number
|
* Limit items to max number
|
||||||
|
* Modifies the array.
|
||||||
*/
|
*/
|
||||||
private cleanupItems(accountId: string): void {
|
const cleanupItems = (items: Item[], accountName: string): Item[] => {
|
||||||
if (!this.storedData.itemsMap.hasOwnProperty(accountId)) {
|
if (accountName === undefined) {
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemsArray = this.storedData.itemsMap[accountId]
|
const itemsArray = items.filter((item) => item.type in Type).slice(0, itemsMaxNumber);
|
||||||
.filter((item) => item.type in Type)
|
|
||||||
.slice(0, MostRecentActivity.itemsMaxNumber);
|
|
||||||
if (itemsArray.length === 0) {
|
if (itemsArray.length === 0) {
|
||||||
delete this.storedData.itemsMap[accountId];
|
deleteState({
|
||||||
} else {
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
this.storedData.itemsMap[accountId] = itemsArray;
|
globalAccountName: accountName,
|
||||||
}
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return itemsArray;
|
||||||
|
};
|
||||||
|
|
||||||
export const mostRecentActivity = new MostRecentActivity();
|
migrateOldData();
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ 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 { makeStyles } from "@fluentui/react-components";
|
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, 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";
|
||||||
@@ -59,6 +57,32 @@ 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(() => ({
|
||||||
@@ -444,18 +468,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...genericPaneProps}>
|
<RightPaneForm {...genericPaneProps}>
|
||||||
<div className="paneMainContent">
|
<div className={`paneMainContent ${styles.container}`}>
|
||||||
|
<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>
|
</div>
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
ariaLabelledBy="pageOptions"
|
ariaLabelledBy="pageOptions"
|
||||||
selectedKey={pageOption}
|
selectedKey={pageOption}
|
||||||
@@ -463,14 +488,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
styles={choiceButtonStyles}
|
styles={choiceButtonStyles}
|
||||||
onChange={handleOnPageOptionChange}
|
onChange={handleOnPageOptionChange}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="tabs settingsSectionPart">
|
<div className={`tabs ${styles.settingsSectionContainer}`}>
|
||||||
{isCustomPageOptionSelected() && (
|
{isCustomPageOptionSelected() && (
|
||||||
<div className="tabcontent">
|
<div className="tabcontent">
|
||||||
<div className="settingsSectionLabel">
|
<div className={styles.settingsSectionDescription}>
|
||||||
Query results per page
|
Query results per page{" "}
|
||||||
<InfoTooltip>Enter the number of query results that should be shown per page.</InfoTooltip>
|
<InfoTooltip className={styles.headerIcon}>
|
||||||
|
Enter the number of query results that should be shown per page.
|
||||||
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SpinButton
|
<SpinButton
|
||||||
@@ -490,36 +516,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{userContext.apiType === "SQL" &&
|
{userContext.apiType === "SQL" &&
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
configContext.platform !== Platform.Fabric && (
|
configContext.platform !== Platform.Fabric && (
|
||||||
<>
|
<AccordionItem value="2">
|
||||||
<div className="settingsSection">
|
<AccordionHeader>
|
||||||
<div className="settingsSectionPart">
|
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||||
<fieldset>
|
</AccordionHeader>
|
||||||
<legend id="enableDataPlaneRBACOptions" className="settingsSectionLabel legendLabel">
|
<AccordionPanel>
|
||||||
Enable Entra ID RBAC
|
<div className={styles.settingsSectionContainer}>
|
||||||
</legend>
|
|
||||||
<TooltipHost
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
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>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon iconName="Info" ariaLabel="Info tooltip" className="panelInfoIcon" tabIndex={0} />
|
|
||||||
</TooltipHost>
|
|
||||||
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
|
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarType={MessageBarType.warning}
|
messageBarType={MessageBarType.warning}
|
||||||
@@ -531,6 +539,18 @@ 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}
|
||||||
@@ -538,58 +558,23 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
selectedKey={enableDataPlaneRBACOption}
|
selectedKey={enableDataPlaneRBACOption}
|
||||||
onChange={handleOnDataPlaneRBACOptionChange}
|
onChange={handleOnDataPlaneRBACOptionChange}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
</>
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<>
|
<>
|
||||||
<div className="settingsSection">
|
<AccordionItem value="3">
|
||||||
<div className="settingsSectionPart">
|
<AccordionHeader>
|
||||||
<div>
|
<div className={styles.header}>Query Timeout</div>
|
||||||
<legend id="ruThresholdLabel" className="settingsSectionLabel legendLabel">
|
</AccordionHeader>
|
||||||
RU Threshold
|
<AccordionPanel>
|
||||||
</legend>
|
<div className={styles.settingsSectionContainer}>
|
||||||
<InfoTooltip>If a query exceeds a configured RU threshold, the query will be aborted.</InfoTooltip>
|
<div className={styles.settingsSectionDescription}>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Toggle
|
|
||||||
styles={toggleStyles}
|
|
||||||
label="Enable RU threshold"
|
|
||||||
onChange={handleOnRUThresholdToggleChange}
|
|
||||||
defaultChecked={ruThresholdEnabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{ruThresholdEnabled && (
|
|
||||||
<div>
|
|
||||||
<SpinButton
|
|
||||||
label="RU Threshold (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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="settingsSection">
|
|
||||||
<div className="settingsSectionPart">
|
|
||||||
<div>
|
|
||||||
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
|
|
||||||
Query Timeout
|
|
||||||
</legend>
|
|
||||||
<InfoTooltip>
|
|
||||||
When a query reaches a specified time limit, a popup with an option to cancel the query will show
|
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
|
unless automatic cancellation has been enabled.
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Toggle
|
<Toggle
|
||||||
styles={toggleStyles}
|
styles={toggleStyles}
|
||||||
label="Enable query timeout"
|
label="Enable query timeout"
|
||||||
@@ -598,7 +583,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{queryTimeoutEnabled && (
|
{queryTimeoutEnabled && (
|
||||||
<div>
|
<div className={styles.settingsSectionContainer}>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
label="Query timeout (ms)"
|
label="Query timeout (ms)"
|
||||||
labelPosition={Position.top}
|
labelPosition={Position.top}
|
||||||
@@ -618,17 +603,52 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
<Toggle
|
||||||
|
styles={toggleStyles}
|
||||||
|
label="Enable RU limit"
|
||||||
|
onChange={handleOnRUThresholdToggleChange}
|
||||||
|
defaultChecked={ruThresholdEnabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="settingsSection">
|
{ruThresholdEnabled && (
|
||||||
<div className="settingsSectionPart">
|
<div className={styles.settingsSectionContainer}>
|
||||||
<div>
|
<SpinButton
|
||||||
<legend id="defaultQueryResultsView" className="settingsSectionLabel legendLabel">
|
label="RU Limit (RU)"
|
||||||
Default Query Results View
|
labelPosition={Position.top}
|
||||||
</legend>
|
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
|
||||||
<InfoTooltip>Select the default view to use when displaying query results.</InfoTooltip>
|
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>
|
</div>
|
||||||
<div>
|
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
ariaLabelledBy="defaultQueryResultsView"
|
ariaLabelledBy="defaultQueryResultsView"
|
||||||
selectedKey={defaultQueryResultsView}
|
selectedKey={defaultQueryResultsView}
|
||||||
@@ -637,21 +657,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
onChange={handleOnDefaultQueryResultsViewChange}
|
onChange={handleOnDefaultQueryResultsViewChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
</div>
|
</AccordionItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="settingsSection">
|
|
||||||
<div className="settingsSectionPart">
|
<AccordionItem value="6">
|
||||||
<div className="settingsSectionLabel">
|
<AccordionHeader>
|
||||||
Retry Settings
|
<div className={styles.header}>Retry Settings</div>
|
||||||
<InfoTooltip>Retry policy associated with throttled requests during CosmosDB queries.</InfoTooltip>
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div className={styles.settingsSectionContainer}>
|
||||||
|
<div className={styles.settingsSectionDescription}>
|
||||||
|
Retry policy associated with throttled requests during CosmosDB queries.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
<span className={styles.subHeader}>Max retry attempts</span>
|
||||||
Max retry attempts
|
<InfoTooltip className={styles.headerIcon}>
|
||||||
</legend>
|
Max number of retries to be performed for a request. Default value 9.
|
||||||
<InfoTooltip>Max number of retries to be performed for a request. Default value 9.</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
labelPosition={Position.top}
|
labelPosition={Position.top}
|
||||||
@@ -667,12 +691,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
styles={spinButtonStyles}
|
styles={spinButtonStyles}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
||||||
Fixed retry interval (ms)
|
<InfoTooltip className={styles.headerIcon}>
|
||||||
</legend>
|
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as
|
||||||
<InfoTooltip>
|
part of the response. Default value is 0 milliseconds.
|
||||||
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>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
@@ -689,10 +711,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
styles={spinButtonStyles}
|
styles={spinButtonStyles}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
<span className={styles.subHeader}>Max wait time (s)</span>
|
||||||
Max wait time (s)
|
<InfoTooltip className={styles.headerIcon}>
|
||||||
</legend>
|
|
||||||
<InfoTooltip>
|
|
||||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||||
seconds.
|
seconds.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
@@ -711,14 +731,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
styles={spinButtonStyles}
|
styles={spinButtonStyles}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
<div className="settingsSection">
|
</AccordionItem>
|
||||||
<div className="settingsSectionPart settingsSectionInlineCheckbox">
|
|
||||||
<div className="settingsSectionLabel">
|
<AccordionItem value="7">
|
||||||
Enable container pagination
|
<AccordionHeader>
|
||||||
<InfoTooltip>
|
<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.
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
styles={{
|
styles={{
|
||||||
@@ -728,20 +751,23 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
ariaLabel="Enable container pagination"
|
ariaLabel="Enable container pagination"
|
||||||
checked={containerPaginationEnabled}
|
checked={containerPaginationEnabled}
|
||||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||||
|
label="Enable container pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
{shouldShowCrossPartitionOption && (
|
</AccordionItem>
|
||||||
<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>
|
|
||||||
|
|
||||||
|
{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
|
<Checkbox
|
||||||
styles={{
|
styles={{
|
||||||
label: { padding: 0 },
|
label: { padding: 0 },
|
||||||
@@ -750,22 +776,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
ariaLabel="Enable cross partition query"
|
ariaLabel="Enable cross partition query"
|
||||||
checked={crossPartitionQueryEnabled}
|
checked={crossPartitionQueryEnabled}
|
||||||
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
|
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
|
||||||
|
label="Enable cross-partition query"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowParallelismOption && (
|
{shouldShowParallelismOption && (
|
||||||
<div className="settingsSection">
|
<AccordionItem value="9">
|
||||||
<div className="settingsSectionPart">
|
<AccordionHeader>
|
||||||
<div className="settingsSectionLabel">
|
<div className={styles.header}>Max degree of parallelism</div>
|
||||||
Max degree of parallelism
|
</AccordionHeader>
|
||||||
<InfoTooltip>
|
<AccordionPanel>
|
||||||
|
<div className={styles.settingsSectionContainer}>
|
||||||
|
<div className={styles.settingsSectionDescription}>
|
||||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A
|
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
|
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.
|
less than 0, the system automatically decides the number of concurrent operations to run.
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SpinButton
|
<SpinButton
|
||||||
min={-1}
|
min={-1}
|
||||||
step={1}
|
step={1}
|
||||||
@@ -773,26 +802,33 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
role="textbox"
|
role="textbox"
|
||||||
id="max-degree"
|
id="max-degree"
|
||||||
value={"" + maxDegreeOfParallelism}
|
value={"" + maxDegreeOfParallelism}
|
||||||
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
|
onIncrement={(newValue) =>
|
||||||
onDecrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)}
|
setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)
|
||||||
|
}
|
||||||
|
onDecrement={(newValue) =>
|
||||||
|
setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)
|
||||||
|
}
|
||||||
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
|
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
|
||||||
ariaLabel="Max degree of parallelism"
|
ariaLabel="Max degree of parallelism"
|
||||||
|
label="Max degree of parallelism"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowPriorityLevelOption && (
|
{shouldShowPriorityLevelOption && (
|
||||||
<div className="settingsSection">
|
<AccordionItem value="10">
|
||||||
<div className="settingsSectionPart">
|
<AccordionHeader>
|
||||||
<fieldset>
|
<div className={styles.header}>Priority Level</div>
|
||||||
<legend id="priorityLevel" className="settingsSectionLabel legendLabel">
|
</AccordionHeader>
|
||||||
Priority Level
|
<AccordionPanel>
|
||||||
</legend>
|
<div className={styles.settingsSectionContainer}>
|
||||||
<InfoTooltip>
|
<div className={styles.settingsSectionDescription}>
|
||||||
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
|
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
|
Execution. If "None" is selected, Data Explorer will not specify priority level, and the
|
||||||
server-side default priority level will be used.
|
server-side default priority level will be used.
|
||||||
</InfoTooltip>
|
</div>
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
ariaLabelledBy="priorityLevel"
|
ariaLabelledBy="priorityLevel"
|
||||||
selectedKey={priorityLevel}
|
selectedKey={priorityLevel}
|
||||||
@@ -800,21 +836,22 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
styles={choiceButtonStyles}
|
styles={choiceButtonStyles}
|
||||||
onChange={handleOnPriorityLevelOptionChange}
|
onChange={handleOnPriorityLevelOptionChange}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{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>
|
|
||||||
|
|
||||||
|
{shouldShowGraphAutoVizOption && (
|
||||||
|
<AccordionItem value="11">
|
||||||
|
<AccordionHeader>
|
||||||
|
<div className={styles.header}>Display Gremlin query results as: </div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div className={styles.settingsSectionContainer}>
|
||||||
|
<div className={styles.settingsSectionDescription}>
|
||||||
|
Select Graph to automatically visualize the query results as a Graph or JSON to display the results
|
||||||
|
as JSON.
|
||||||
|
</div>
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
selectedKey={graphAutoVizDisabled}
|
selectedKey={graphAutoVizDisabled}
|
||||||
options={graphAutoOptionList}
|
options={graphAutoOptionList}
|
||||||
@@ -822,20 +859,22 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
aria-label="Graph Auto-visualization"
|
aria-label="Graph Auto-visualization"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
{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>
|
|
||||||
|
|
||||||
|
{shouldShowCopilotSampleDBOption && (
|
||||||
|
<AccordionItem value="12">
|
||||||
|
<AccordionHeader>
|
||||||
|
<div className={styles.header}>Enable sample database</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div className={styles.settingsSectionContainer}>
|
||||||
|
<div className={styles.settingsSectionDescription}>
|
||||||
|
This is a sample database and collection with synthetic product data you can use to explore using
|
||||||
|
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and
|
||||||
|
is created by, and maintained by Microsoft at no cost to you.
|
||||||
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
styles={{
|
styles={{
|
||||||
label: { padding: 0 },
|
label: { padding: 0 },
|
||||||
@@ -844,10 +883,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
ariaLabel="Enable sample db for Query Advisor"
|
ariaLabel="Enable sample db for Query Advisor"
|
||||||
checked={copilotSampleDBEnabled}
|
checked={copilotSampleDBEnabled}
|
||||||
onChange={handleSampleDatabaseChange}
|
onChange={handleSampleDatabaseChange}
|
||||||
|
label="Enable sample database"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart">
|
<div className="settingsSectionPart">
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
|
|||||||
@@ -8,24 +8,30 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
submitButtonText="Apply"
|
submitButtonText="Apply"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="paneMainContent"
|
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
||||||
>
|
>
|
||||||
|
<Accordion
|
||||||
|
className="___1uf6361_0000000 fz7g6wx"
|
||||||
|
>
|
||||||
|
<AccordionItem
|
||||||
|
value="1"
|
||||||
|
>
|
||||||
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="___15c001r_0000000 fq02s40"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionPart"
|
|
||||||
>
|
|
||||||
<fieldset>
|
|
||||||
<legend
|
|
||||||
className="settingsSectionLabel legendLabel"
|
|
||||||
id="pageOptions"
|
|
||||||
>
|
>
|
||||||
Page Options
|
Page Options
|
||||||
</legend>
|
</div>
|
||||||
<InfoTooltip>
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
|
>
|
||||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
|
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
|
||||||
</InfoTooltip>
|
</div>
|
||||||
<StyledChoiceGroup
|
<StyledChoiceGroup
|
||||||
ariaLabelledBy="pageOptions"
|
ariaLabelledBy="pageOptions"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -66,19 +72,21 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="tabs settingsSectionPart"
|
className="tabs ___1dfa554_0000000 fo7qwa0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="tabcontent"
|
className="tabcontent"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionLabel"
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
>
|
>
|
||||||
Query results per page
|
Query results per page
|
||||||
<InfoTooltip>
|
|
||||||
|
<InfoTooltip
|
||||||
|
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
||||||
|
>
|
||||||
Enter the number of query results that should be shown per page.
|
Enter the number of query results that should be shown per page.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,28 +104,30 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem
|
||||||
|
value="3"
|
||||||
|
>
|
||||||
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="___15c001r_0000000 fq02s40"
|
||||||
|
>
|
||||||
|
Query Timeout
|
||||||
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart"
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
>
|
>
|
||||||
<div>
|
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.
|
||||||
<legend
|
|
||||||
className="settingsSectionLabel legendLabel"
|
|
||||||
id="ruThresholdLabel"
|
|
||||||
>
|
|
||||||
RU Threshold
|
|
||||||
</legend>
|
|
||||||
<InfoTooltip>
|
|
||||||
If a query exceeds a configured RU threshold, the query will be aborted.
|
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<StyledToggleBase
|
<StyledToggleBase
|
||||||
defaultChecked={true}
|
defaultChecked={false}
|
||||||
label="Enable RU threshold"
|
label="Enable query timeout"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
@@ -135,12 +145,55 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem
|
||||||
|
value="4"
|
||||||
|
>
|
||||||
|
<AccordionHeader>
|
||||||
|
<div
|
||||||
|
className="___15c001r_0000000 fq02s40"
|
||||||
|
>
|
||||||
|
RU Limit
|
||||||
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
|
>
|
||||||
|
If a query exceeds a configured RU limit, the query will be aborted.
|
||||||
|
</div>
|
||||||
|
<StyledToggleBase
|
||||||
|
defaultChecked={true}
|
||||||
|
label="Enable RU limit"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
{
|
||||||
|
"container": {},
|
||||||
|
"label": {
|
||||||
|
"display": "block",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 400,
|
||||||
|
},
|
||||||
|
"pill": {},
|
||||||
|
"root": {},
|
||||||
|
"text": {},
|
||||||
|
"thumb": {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
<StyledSpinButton
|
<StyledSpinButton
|
||||||
decrementButtonAriaLabel="Decrease value by 1000"
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
defaultValue="5000"
|
defaultValue="5000"
|
||||||
incrementButtonAriaLabel="Increase value by 1000"
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
label="RU Threshold (RU)"
|
label="RU Limit (RU)"
|
||||||
labelPosition={0}
|
labelPosition={0}
|
||||||
min={1}
|
min={1}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -163,66 +216,27 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
</div>
|
</AccordionItem>
|
||||||
|
<AccordionItem
|
||||||
|
value="5"
|
||||||
|
>
|
||||||
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="___15c001r_0000000 fq02s40"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionPart"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<legend
|
|
||||||
className="settingsSectionLabel legendLabel"
|
|
||||||
id="queryTimeoutLabel"
|
|
||||||
>
|
|
||||||
Query Timeout
|
|
||||||
</legend>
|
|
||||||
<InfoTooltip>
|
|
||||||
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
|
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<StyledToggleBase
|
|
||||||
defaultChecked={false}
|
|
||||||
label="Enable query timeout"
|
|
||||||
onChange={[Function]}
|
|
||||||
styles={
|
|
||||||
{
|
|
||||||
"container": {},
|
|
||||||
"label": {
|
|
||||||
"display": "block",
|
|
||||||
"fontSize": 12,
|
|
||||||
"fontWeight": 400,
|
|
||||||
},
|
|
||||||
"pill": {},
|
|
||||||
"root": {},
|
|
||||||
"text": {},
|
|
||||||
"thumb": {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="settingsSection"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionPart"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<legend
|
|
||||||
className="settingsSectionLabel legendLabel"
|
|
||||||
id="defaultQueryResultsView"
|
|
||||||
>
|
>
|
||||||
Default Query Results View
|
Default Query Results View
|
||||||
</legend>
|
|
||||||
<InfoTooltip>
|
|
||||||
Select the default view to use when displaying query results.
|
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
|
>
|
||||||
|
Select the default view to use when displaying query results.
|
||||||
|
</div>
|
||||||
<StyledChoiceGroup
|
<StyledChoiceGroup
|
||||||
ariaLabelledBy="defaultQueryResultsView"
|
ariaLabelledBy="defaultQueryResultsView"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -264,30 +278,36 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
</div>
|
</AccordionItem>
|
||||||
<div
|
<AccordionItem
|
||||||
className="settingsSection"
|
value="6"
|
||||||
>
|
>
|
||||||
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart"
|
className="___15c001r_0000000 fq02s40"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionLabel"
|
|
||||||
>
|
>
|
||||||
Retry Settings
|
Retry Settings
|
||||||
<InfoTooltip>
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
|
>
|
||||||
Retry policy associated with throttled requests during CosmosDB queries.
|
Retry policy associated with throttled requests during CosmosDB queries.
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<legend
|
<span
|
||||||
className="settingsSectionLabel legendLabel"
|
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
||||||
id="queryRetryAttemptsLabel"
|
|
||||||
>
|
>
|
||||||
Max retry attempts
|
Max retry attempts
|
||||||
</legend>
|
</span>
|
||||||
<InfoTooltip>
|
<InfoTooltip
|
||||||
|
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
||||||
|
>
|
||||||
Max number of retries to be performed for a request. Default value 9.
|
Max number of retries to be performed for a request. Default value 9.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,13 +340,14 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
value="9"
|
value="9"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<legend
|
<span
|
||||||
className="settingsSectionLabel legendLabel"
|
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
||||||
id="queryRetryAttemptsLabel"
|
|
||||||
>
|
>
|
||||||
Fixed retry interval (ms)
|
Fixed retry interval (ms)
|
||||||
</legend>
|
</span>
|
||||||
<InfoTooltip>
|
<InfoTooltip
|
||||||
|
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
||||||
|
>
|
||||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. Default value is 0 milliseconds.
|
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>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,13 +380,14 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
value="0"
|
value="0"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<legend
|
<span
|
||||||
className="settingsSectionLabel legendLabel"
|
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
||||||
id="queryRetryAttemptsLabel"
|
|
||||||
>
|
>
|
||||||
Max wait time (s)
|
Max wait time (s)
|
||||||
</legend>
|
</span>
|
||||||
<InfoTooltip>
|
<InfoTooltip
|
||||||
|
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
||||||
|
>
|
||||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.
|
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -398,25 +420,32 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
value="30"
|
value="30"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
<div
|
</AccordionItem>
|
||||||
className="settingsSection"
|
<AccordionItem
|
||||||
|
value="7"
|
||||||
>
|
>
|
||||||
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart settingsSectionInlineCheckbox"
|
className="___15c001r_0000000 fq02s40"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionLabel"
|
|
||||||
>
|
>
|
||||||
Enable container pagination
|
Enable container pagination
|
||||||
<InfoTooltip>
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
|
>
|
||||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
ariaLabel="Enable container pagination"
|
ariaLabel="Enable container pagination"
|
||||||
checked={false}
|
checked={false}
|
||||||
className="padding"
|
className="padding"
|
||||||
|
label="Enable container pagination"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
@@ -427,25 +456,32 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
<div
|
</AccordionItem>
|
||||||
className="settingsSection"
|
<AccordionItem
|
||||||
|
value="8"
|
||||||
>
|
>
|
||||||
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart settingsSectionInlineCheckbox"
|
className="___15c001r_0000000 fq02s40"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionLabel"
|
|
||||||
>
|
>
|
||||||
Enable cross-partition query
|
Enable cross-partition query
|
||||||
<InfoTooltip>
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
|
>
|
||||||
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.
|
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>
|
</div>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
ariaLabel="Enable cross partition query"
|
ariaLabel="Enable cross partition query"
|
||||||
checked={false}
|
checked={false}
|
||||||
className="padding"
|
className="padding"
|
||||||
|
label="Enable cross-partition query"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
@@ -456,25 +492,32 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
<div
|
</AccordionItem>
|
||||||
className="settingsSection"
|
<AccordionItem
|
||||||
|
value="9"
|
||||||
>
|
>
|
||||||
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart"
|
className="___15c001r_0000000 fq02s40"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionLabel"
|
|
||||||
>
|
>
|
||||||
Max degree of parallelism
|
Max degree of parallelism
|
||||||
<InfoTooltip>
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
|
>
|
||||||
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.
|
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.
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<StyledSpinButton
|
<StyledSpinButton
|
||||||
ariaLabel="Max degree of parallelism"
|
ariaLabel="Max degree of parallelism"
|
||||||
className="textfontclr"
|
className="textfontclr"
|
||||||
id="max-degree"
|
id="max-degree"
|
||||||
|
label="Max degree of parallelism"
|
||||||
min={-1}
|
min={-1}
|
||||||
onDecrement={[Function]}
|
onDecrement={[Function]}
|
||||||
onIncrement={[Function]}
|
onIncrement={[Function]}
|
||||||
@@ -484,7 +527,9 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
value="6"
|
value="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
@@ -524,30 +569,39 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
submitButtonText="Apply"
|
submitButtonText="Apply"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="paneMainContent"
|
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
||||||
>
|
>
|
||||||
<div
|
<Accordion
|
||||||
className="settingsSection"
|
className="___1uf6361_0000000 fz7g6wx"
|
||||||
>
|
>
|
||||||
<div
|
<AccordionItem
|
||||||
className="settingsSectionPart"
|
value="6"
|
||||||
>
|
>
|
||||||
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionLabel"
|
className="___15c001r_0000000 fq02s40"
|
||||||
>
|
>
|
||||||
Retry Settings
|
Retry Settings
|
||||||
<InfoTooltip>
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
|
>
|
||||||
Retry policy associated with throttled requests during CosmosDB queries.
|
Retry policy associated with throttled requests during CosmosDB queries.
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<legend
|
<span
|
||||||
className="settingsSectionLabel legendLabel"
|
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
||||||
id="queryRetryAttemptsLabel"
|
|
||||||
>
|
>
|
||||||
Max retry attempts
|
Max retry attempts
|
||||||
</legend>
|
</span>
|
||||||
<InfoTooltip>
|
<InfoTooltip
|
||||||
|
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
||||||
|
>
|
||||||
Max number of retries to be performed for a request. Default value 9.
|
Max number of retries to be performed for a request. Default value 9.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -580,13 +634,14 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
value="9"
|
value="9"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<legend
|
<span
|
||||||
className="settingsSectionLabel legendLabel"
|
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
||||||
id="queryRetryAttemptsLabel"
|
|
||||||
>
|
>
|
||||||
Fixed retry interval (ms)
|
Fixed retry interval (ms)
|
||||||
</legend>
|
</span>
|
||||||
<InfoTooltip>
|
<InfoTooltip
|
||||||
|
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
||||||
|
>
|
||||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. Default value is 0 milliseconds.
|
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>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -619,13 +674,14 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
value="0"
|
value="0"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<legend
|
<span
|
||||||
className="settingsSectionLabel legendLabel"
|
className="___zls1px0_0000000 fq02s40 f1ugzwwg"
|
||||||
id="queryRetryAttemptsLabel"
|
|
||||||
>
|
>
|
||||||
Max wait time (s)
|
Max wait time (s)
|
||||||
</legend>
|
</span>
|
||||||
<InfoTooltip>
|
<InfoTooltip
|
||||||
|
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
|
||||||
|
>
|
||||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.
|
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -658,25 +714,32 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
value="30"
|
value="30"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
<div
|
</AccordionItem>
|
||||||
className="settingsSection"
|
<AccordionItem
|
||||||
|
value="7"
|
||||||
>
|
>
|
||||||
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart settingsSectionInlineCheckbox"
|
className="___15c001r_0000000 fq02s40"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionLabel"
|
|
||||||
>
|
>
|
||||||
Enable container pagination
|
Enable container pagination
|
||||||
<InfoTooltip>
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
|
>
|
||||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
ariaLabel="Enable container pagination"
|
ariaLabel="Enable container pagination"
|
||||||
checked={false}
|
checked={false}
|
||||||
className="padding"
|
className="padding"
|
||||||
|
label="Enable container pagination"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
{
|
{
|
||||||
@@ -687,20 +750,26 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
<div
|
</AccordionItem>
|
||||||
className="settingsSection"
|
<AccordionItem
|
||||||
|
value="11"
|
||||||
>
|
>
|
||||||
|
<AccordionHeader>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart"
|
className="___15c001r_0000000 fq02s40"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionLabel"
|
|
||||||
>
|
>
|
||||||
Display Gremlin query results as:
|
Display Gremlin query results as:
|
||||||
<InfoTooltip>
|
</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionPanel>
|
||||||
|
<div
|
||||||
|
className="___1dfa554_0000000 fo7qwa0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||||
|
>
|
||||||
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as JSON.
|
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as JSON.
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<StyledChoiceGroup
|
<StyledChoiceGroup
|
||||||
aria-label="Graph Auto-visualization"
|
aria-label="Graph Auto-visualization"
|
||||||
@@ -720,7 +789,9 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
selectedKey="false"
|
selectedKey="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -61,7 +61,15 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
|
|||||||
if (checked) {
|
if (checked) {
|
||||||
selectedColumnIdsSet.add(id);
|
selectedColumnIdsSet.add(id);
|
||||||
} else {
|
} else {
|
||||||
if (selectedColumnIdsSet.size === 1 && selectedColumnIdsSet.has(id)) {
|
/* 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
|
// Don't allow unchecking the last column
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +28,8 @@ 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";
|
||||||
@@ -136,9 +138,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||||
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
const [histories, setHistories] = useState<string[]>(() => readPromptHistory(userContext.databaseAccount));
|
||||||
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);
|
||||||
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
|
savePromptHistory(userContext.databaseAccount, newHistories);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetMessageStates = (): void => {
|
const resetMessageStates = (): void => {
|
||||||
|
|||||||
@@ -1,10 +1,39 @@
|
|||||||
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,9 +3,10 @@ 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/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
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";
|
||||||
@@ -18,18 +19,13 @@ 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 cachedCopilotToggleStatus: string = localStorage.getItem(
|
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
||||||
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
readCopilotToggleStatus(userContext.databaseAccount),
|
||||||
);
|
);
|
||||||
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[] => {
|
||||||
@@ -88,7 +84,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
|
|
||||||
const toggleCopilot = (toggle: boolean) => {
|
const toggleCopilot = (toggle: boolean) => {
|
||||||
setCopilotActive(toggle);
|
setCopilotActive(toggle);
|
||||||
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, toggle.toString());
|
saveCopilotToggleStatus(userContext.databaseAccount, toggle);
|
||||||
};
|
};
|
||||||
|
|
||||||
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,8 +4,11 @@ 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;
|
||||||
@@ -54,3 +57,110 @@ 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 } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readCopilotToggleStatus } 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,7 +36,6 @@ 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,
|
||||||
@@ -361,9 +360,7 @@ export const QueryDocumentsPerPage = async (
|
|||||||
correlationId: useQueryCopilot.getState().correlationId,
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isCopilotActive = StringUtility.toBoolean(
|
const isCopilotActive = readCopilotToggleStatus(userContext.databaseAccount);
|
||||||
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,38 +17,6 @@ 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 } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
const useSidebarStyles = makeStyles({
|
const useSidebarStyles = makeStyles({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
@@ -113,6 +113,12 @@ interface GlobalCommand {
|
|||||||
|
|
||||||
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||||
const styles = useSidebarStyles();
|
const styles = useSidebarStyles();
|
||||||
|
|
||||||
|
// Since we have two buttons in the DOM (one for small screens and one for larger screens), we wrap the entire thing in a div.
|
||||||
|
// However, that messes with the Menu positioning, so we need to get a reference to the 'div' to pass to the Menu.
|
||||||
|
// We can't use a ref though, because it would be set after the Menu is rendered, so we use a state value to force a re-render.
|
||||||
|
const [globalCommandButton, setGlobalCommandButton] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
const actions = useMemo<GlobalCommand[]>(() => {
|
const actions = useMemo<GlobalCommand[]>(() => {
|
||||||
if (
|
if (
|
||||||
configContext.platform === Platform.Fabric ||
|
configContext.platform === Platform.Fabric ||
|
||||||
@@ -182,10 +188,10 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
{primaryAction.label}
|
{primaryAction.label}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Menu positioning="below-end">
|
<Menu positioning={{ target: globalCommandButton, position: "below", align: "end" }}>
|
||||||
<MenuTrigger disableButtonEnhancement>
|
<MenuTrigger disableButtonEnhancement>
|
||||||
{(triggerProps: MenuButtonProps) => (
|
{(triggerProps: MenuButtonProps) => (
|
||||||
<>
|
<div ref={setGlobalCommandButton}>
|
||||||
<SplitButton
|
<SplitButton
|
||||||
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
menuButton={{ ...triggerProps, "aria-label": "More commands" }}
|
||||||
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
primaryActionButton={{ onClick: onPrimaryActionClick }}
|
||||||
@@ -197,7 +203,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>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private clearMostRecent = (): void => {
|
private clearMostRecent = (): void => {
|
||||||
MostRecentActivity.mostRecentActivity.clear(userContext.databaseAccount?.id);
|
MostRecentActivity.clear(userContext.databaseAccount?.name);
|
||||||
this.setState({});
|
this.setState({});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -498,7 +498,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createRecentItems(): SplashScreenItem[] {
|
private createRecentItems(): SplashScreenItem[] {
|
||||||
return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => {
|
return MostRecentActivity.getItems(userContext.databaseAccount?.name).map((activity) => {
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
default: {
|
default: {
|
||||||
const unknownActivity: never = activity;
|
const unknownActivity: never = activity;
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ 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,6 +193,9 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -205,6 +208,10 @@ 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(),
|
||||||
@@ -370,8 +377,9 @@ 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))) {
|
if (table.isItemSelected(table.getTableEntityKeys(rowKey, partitionKey))) {
|
||||||
$row.attr("tabindex", "0");
|
$row.attr("tabindex", "0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ 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(lastSelectedItem.RowKey._),
|
this._tableEntityListViewModel.getTableEntityKeys(
|
||||||
|
lastSelectedItem.RowKey._,
|
||||||
|
lastSelectedItem.PartitionKey && lastSelectedItem.PartitionKey._,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: -1;
|
: -1;
|
||||||
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
||||||
@@ -147,13 +150,14 @@ 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.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
this._tableEntityListViewModel.lastSelectedItem = entity;
|
this._tableEntityListViewModel.lastSelectedItem = entity;
|
||||||
@@ -168,7 +172,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);
|
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,11 +194,11 @@ export default class DataTableOperationManager {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!this._tableEntityListViewModel.isItemSelected(
|
!this._tableEntityListViewModel.isItemSelected(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Adding item not previously in selection
|
// Adding item not previously in selection
|
||||||
this.addToSelection(entityIdentity.RowKey);
|
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
||||||
} else {
|
} else {
|
||||||
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
||||||
}
|
}
|
||||||
@@ -212,10 +216,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.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
||||||
);
|
);
|
||||||
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._),
|
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.PartitionKey._, anchorItem.RowKey._),
|
||||||
);
|
);
|
||||||
|
|
||||||
var startIndex = Math.min(elementIndex, anchorIndex);
|
var startIndex = Math.min(elementIndex, anchorIndex);
|
||||||
@@ -234,24 +238,25 @@ export default class DataTableOperationManager {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!this._tableEntityListViewModel.isItemSelected(
|
!this._tableEntityListViewModel.isItemSelected(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.PartitionKey, entityIdentity.RowKey),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (this._tableEntityListViewModel.selected().length) {
|
if (this._tableEntityListViewModel.selected().length) {
|
||||||
this._tableEntityListViewModel.clearSelection();
|
this._tableEntityListViewModel.clearSelection();
|
||||||
}
|
}
|
||||||
this.addToSelection(entityIdentity.RowKey);
|
this.addToSelection(entityIdentity.RowKey, entityIdentity.PartitionKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addToSelection(rowKey: string) {
|
private addToSelection(rowKey: string, partitionKey?: string) {
|
||||||
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(rowKey),
|
this._tableEntityListViewModel.getTableEntityKeys(rowKey, partitionKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
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.
|
||||||
@@ -269,7 +274,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._);
|
this.addToSelection(firstEntity.RowKey._, firstEntity.PartitionKey && firstEntity.PartitionKey._);
|
||||||
|
|
||||||
// Update last selection
|
// Update last selection
|
||||||
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
||||||
|
|||||||
@@ -128,8 +128,14 @@ 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): Entities.IProperty[] {
|
public getTableEntityKeys(rowKey: string, partitionKey: string): Entities.IProperty[] {
|
||||||
return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
|
const properties: Entities.IProperty[] = [{ 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> {
|
||||||
@@ -261,7 +267,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
}
|
}
|
||||||
var oldEntityIndex: number = _.findIndex(
|
var oldEntityIndex: number = _.findIndex(
|
||||||
this.cache.data,
|
this.cache.data,
|
||||||
(data: Entities.ITableEntity) => data.RowKey._ === entity.RowKey._,
|
(data: Entities.ITableEntity) =>
|
||||||
|
data.RowKey._ === entity.RowKey._ && data.PartitionKey._ === entity.PartitionKey._,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cache.data.splice(oldEntityIndex, 1, entity);
|
this.cache.data.splice(oldEntityIndex, 1, entity);
|
||||||
@@ -285,7 +292,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: Entities.ITableEntity) => e.RowKey._ === entity.RowKey._ && e.PartitionKey._ === entity.PartitionKey._,
|
||||||
);
|
);
|
||||||
if (cachedIndex >= 0) {
|
if (cachedIndex >= 0) {
|
||||||
this.cache.data.splice(cachedIndex, 1);
|
this.cache.data.splice(cachedIndex, 1);
|
||||||
@@ -393,6 +400,16 @@ 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,4 +36,5 @@ export interface ITableQuery {
|
|||||||
|
|
||||||
export interface ITableEntityIdentity {
|
export interface ITableEntityIdentity {
|
||||||
RowKey: string;
|
RowKey: string;
|
||||||
|
PartitionKey?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -753,17 +753,11 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
CassandraProxyEndpoints.Development,
|
CassandraProxyEndpoints.Development,
|
||||||
CassandraProxyEndpoints.Mpac,
|
CassandraProxyEndpoints.Mpac,
|
||||||
CassandraProxyEndpoints.Prod,
|
CassandraProxyEndpoints.Prod,
|
||||||
|
CassandraProxyEndpoints.Fairfax,
|
||||||
|
CassandraProxyEndpoints.Mooncake,
|
||||||
];
|
];
|
||||||
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
|
||||||
if (
|
|
||||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development &&
|
|
||||||
userContext.databaseAccount.properties.ipRules?.length > 0
|
|
||||||
) {
|
|
||||||
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
canAccessCassandraProxy &&
|
|
||||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
// Definitions of State data
|
// Definitions of State data
|
||||||
|
|
||||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
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 = "DocumentsTab";
|
const componentName = AppStateComponentNames.DocumentsTab;
|
||||||
|
|
||||||
export enum SubComponentName {
|
export enum SubComponentName {
|
||||||
ColumnSizes = "ColumnSizes",
|
ColumnSizes = "ColumnSizes",
|
||||||
FilterHistory = "FilterHistory",
|
FilterHistory = "FilterHistory",
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
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 { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||||
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import {
|
import {
|
||||||
ButtonsDependencies,
|
ButtonsDependencies,
|
||||||
DELETE_BUTTON_ID,
|
DELETE_BUTTON_ID,
|
||||||
@@ -65,12 +68,14 @@ 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(() => ({
|
getState: jest.fn(() => mockDialogState),
|
||||||
showOkCancelModalDialog: (title: string, subText: string, okLabel: string, onOk: () => void) => onOk(),
|
|
||||||
showOkModalDialog: () => {},
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -80,6 +85,10 @@ 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 () => {
|
||||||
@@ -452,7 +461,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);
|
.onCommandClick(undefined, 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();
|
||||||
});
|
});
|
||||||
@@ -462,14 +471,36 @@ 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);
|
.onCommandClick(undefined, 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", () => {
|
it("clicking Delete Document asks for confirmation", async () => {
|
||||||
|
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();
|
||||||
|
|
||||||
@@ -477,10 +508,11 @@ 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);
|
.onCommandClick(undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockDeleteDocuments).toHaveBeenCalled();
|
// The implementation uses setTimeout, so wait for it to finish
|
||||||
|
waitFor(() => expect(mockDeleteDocuments).toHaveBeenCalled());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { Button, Input, TableRowId, makeStyles, shorthands } from "@fluentui/react-components";
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Link,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarBody,
|
||||||
|
MessageBarTitle,
|
||||||
|
TableRowId,
|
||||||
|
makeStyles,
|
||||||
|
shorthands,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
import { Dismiss16Filled } from "@fluentui/react-icons";
|
import { Dismiss16Filled } from "@fluentui/react-icons";
|
||||||
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
@@ -16,8 +26,9 @@ 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/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import {
|
import {
|
||||||
ColumnsSelection,
|
ColumnsSelection,
|
||||||
@@ -35,7 +46,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 } from "Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo } 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";
|
||||||
@@ -60,6 +71,9 @@ import TabsBase from "../TabsBase";
|
|||||||
import { ColumnDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
import { ColumnDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
||||||
|
|
||||||
const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we can afford to keep more items than fit on the screen
|
const 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({
|
||||||
@@ -91,6 +105,13 @@ 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",
|
||||||
@@ -103,6 +124,20 @@ export const useDocumentsTabStyles = makeStyles({
|
|||||||
...shorthands.outline("1px", "dotted"),
|
...shorthands.outline("1px", "dotted"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
floatingControlsContainer: {
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
|
floatingControls: {
|
||||||
|
position: "absolute",
|
||||||
|
top: "6px",
|
||||||
|
right: 0,
|
||||||
|
float: "right",
|
||||||
|
backgroundColor: "white",
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
deleteProgressContent: {
|
||||||
|
paddingTop: tokens.spacingVerticalL,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export class DocumentsTabV2 extends TabsBase {
|
export class DocumentsTabV2 extends TabsBase {
|
||||||
@@ -533,7 +568,7 @@ const defaultMongoFilters = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"];
|
|||||||
type ExtendedDocumentId = DocumentId & { tableFields?: DocumentsTableComponentItem };
|
type ExtendedDocumentId = DocumentId & { tableFields?: DocumentsTableComponentItem };
|
||||||
|
|
||||||
// This is based on some heuristics
|
// This is based on some heuristics
|
||||||
const calculateOffset = (columnNumber: number): number => columnNumber * 16 - 29;
|
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> = ({
|
||||||
@@ -602,6 +637,24 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
readSubComponentState<FilterHistory>(SubComponentName.FilterHistory, _collection, [] as FilterHistory),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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(() => {
|
||||||
@@ -627,6 +680,99 @@ 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,
|
||||||
@@ -882,7 +1028,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.then(() => setSelectedRows(new Set([documentIds.length - 1])))
|
.then(() => {
|
||||||
|
setSelectedRows(new Set([documentIds.length - 1]));
|
||||||
|
setClickedRowIndex(documentIds.length - 1);
|
||||||
|
})
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => setIsExecuting(false));
|
||||||
}, [
|
}, [
|
||||||
onExecutionErrorChange,
|
onExecutionErrorChange,
|
||||||
@@ -976,8 +1125,36 @@ 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[]> => {
|
||||||
@@ -988,20 +1165,33 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
});
|
});
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
|
let deletePromise;
|
||||||
|
if (!isPreferredApiMongoDB) {
|
||||||
|
if (partitionKey.systemKey) {
|
||||||
|
// ----------------------------------------------------------------------------------------------------
|
||||||
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
|
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
|
||||||
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should always be called.
|
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should
|
||||||
const _deleteNoSqlDocuments = async (
|
// always be called for NoSQL.
|
||||||
collection: CollectionBase,
|
deletePromise = deleteNoSqlDocument(_collection, toDeleteDocumentIds[0]).then(() => {
|
||||||
toDeleteDocumentIds: DocumentId[],
|
useDialog.getState().showOkModalDialog("Delete document", "Document successfully deleted.");
|
||||||
): Promise<DocumentId[]> => {
|
return [toDeleteDocumentIds[0]];
|
||||||
return partitionKey.systemKey
|
});
|
||||||
? deleteNoSqlDocument(collection, toDeleteDocumentIds[0]).then(() => [toDeleteDocumentIds[0]])
|
// ----------------------------------------------------------------------------------------------------
|
||||||
: deleteNoSqlDocuments(collection, toDeleteDocumentIds);
|
} else {
|
||||||
};
|
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
|
||||||
|
}
|
||||||
const deletePromise = !isPreferredApiMongoDB
|
} else {
|
||||||
? _deleteNoSqlDocuments(_collection, toDeleteDocumentIds)
|
if (isMongoBulkDeleteDisabled) {
|
||||||
: MongoProxyClient.deleteDocuments(
|
// 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,
|
||||||
@@ -1011,6 +1201,8 @@ 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(
|
||||||
@@ -1041,9 +1233,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
throw error;
|
throw error;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => {
|
||||||
|
setIsExecuting(false);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[_collection, isPreferredApiMongoDB, onExecutionErrorChange, tabTitle],
|
[_collection, isPreferredApiMongoDB, onExecutionErrorChange, tabTitle, partitionKey.systemKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteDocuments = useCallback(
|
const deleteDocuments = useCallback(
|
||||||
@@ -1061,14 +1255,25 @@ 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));
|
||||||
},
|
},
|
||||||
@@ -1853,6 +2058,26 @@ 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 => {
|
const onColumnSelectionChange = (newSelectedColumnIds: string[]): void => {
|
||||||
// Do not allow to unselecting all columns
|
// Do not allow to unselecting all columns
|
||||||
if (newSelectedColumnIds.length === 0) {
|
if (newSelectedColumnIds.length === 0) {
|
||||||
@@ -1888,6 +2113,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]);
|
}, [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" }}>
|
||||||
@@ -2007,7 +2239,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
selectedColumnIds={selectedColumnIds}
|
selectedColumnIds={selectedColumnIds}
|
||||||
columnDefinitions={columnDefinitions}
|
columnDefinitions={columnDefinitions}
|
||||||
isRowSelectionDisabled={
|
isRowSelectionDisabled={
|
||||||
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
isBulkDeleteDisabled ||
|
||||||
|
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
||||||
}
|
}
|
||||||
onColumnSelectionChange={onColumnSelectionChange}
|
onColumnSelectionChange={onColumnSelectionChange}
|
||||||
defaultColumnSelection={getInitialColumnSelection()}
|
defaultColumnSelection={getInitialColumnSelection()}
|
||||||
@@ -2051,6 +2284,52 @@ 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/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
import {
|
import {
|
||||||
DELETE_BUTTON_ID,
|
DELETE_BUTTON_ID,
|
||||||
DISCARD_BUTTON_ID,
|
DISCARD_BUTTON_ID,
|
||||||
@@ -49,7 +49,9 @@ jest.mock("Common/MongoProxyClient", () => ({
|
|||||||
id: "id1",
|
id: "id1",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
deleteDocuments: jest.fn(() => Promise.resolve()),
|
deleteDocuments: jest.fn(() => Promise.resolve({ deleteCount: 0, isAcknowledged: true })),
|
||||||
|
ThrottlingError: Error,
|
||||||
|
useMongoProxyEndpoint: jest.fn(() => true),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
||||||
@@ -161,7 +163,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);
|
.onCommandClick(undefined, 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();
|
||||||
});
|
});
|
||||||
@@ -171,14 +173,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);
|
.onCommandClick(undefined, 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", () => {
|
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();
|
||||||
|
|
||||||
@@ -186,7 +188,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);
|
.onCommandClick(undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockDeleteDocuments).toHaveBeenCalled();
|
expect(mockDeleteDocuments).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
TextSortDescendingRegular,
|
TextSortDescendingRegular,
|
||||||
} from "@fluentui/react-icons";
|
} 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 { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane";
|
||||||
import {
|
import {
|
||||||
ColumnSizesMap,
|
ColumnSizesMap,
|
||||||
@@ -50,7 +51,6 @@ import {
|
|||||||
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
@@ -228,7 +228,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
<MenuItem key="refresh" icon={<ArrowClockwise16Regular />} onClick={onRefreshTable}>
|
||||||
Refresh
|
Refresh
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{userContext.features.enableDocumentsTableColumnSelection && (
|
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) && (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<TextSortAscendingRegular />}
|
icon={<TextSortAscendingRegular />}
|
||||||
@@ -251,6 +251,8 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key="keyboardresize"
|
key="keyboardresize"
|
||||||
icon={<TableResizeColumnRegular />}
|
icon={<TableResizeColumnRegular />}
|
||||||
@@ -258,7 +260,8 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
>
|
>
|
||||||
Resize with left/right arrow keys
|
Resize with left/right arrow keys
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{!isColumnSelectionDisabled && (
|
{[Environment.Development, Environment.Mpac].includes(getEnvironment()) &&
|
||||||
|
!isColumnSelectionDisabled && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key="remove"
|
key="remove"
|
||||||
icon={<DeleteRegular />}
|
icon={<DeleteRegular />}
|
||||||
@@ -276,8 +279,6 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
Remove column
|
Remove column
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</MenuPopover>
|
</MenuPopover>
|
||||||
</Menu>
|
</Menu>
|
||||||
@@ -471,8 +472,8 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table noNativeElements sortable {...tableProps}>
|
<Table noNativeElements {...tableProps}>
|
||||||
<TableHeader>
|
<TableHeader className={styles.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
|
||||||
@@ -494,6 +495,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<div className={styles.tableHeaderFiller}></div>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<List
|
<List
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
|||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"height": "100%",
|
"height": "100%",
|
||||||
"width": "calc(100% + -13px)",
|
"width": "calc(100% + -11px)",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ exports[`DocumentsTableComponent should not render selection column when isSelec
|
|||||||
noNativeElements={true}
|
noNativeElements={true}
|
||||||
role="grid"
|
role="grid"
|
||||||
size="small"
|
size="small"
|
||||||
sortable={true}
|
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"minWidth": "fit-content",
|
"minWidth": "fit-content",
|
||||||
@@ -75,9 +74,11 @@ exports[`DocumentsTableComponent should not render selection column when isSelec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TableHeader>
|
<TableHeader
|
||||||
|
className="___1gzszts_0000000 f22iagw"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TableHeader ___oeyxrt0_1baslyg ftgm304"
|
className="fui-TableHeader ___1gzszts_39qb7g0 f22iagw"
|
||||||
role="rowgroup"
|
role="rowgroup"
|
||||||
>
|
>
|
||||||
<TableRow
|
<TableRow
|
||||||
@@ -98,6 +99,9 @@ exports[`DocumentsTableComponent should not render selection column when isSelec
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<div
|
||||||
|
className="___1ndi7nn_0000000 f64fuq3 f1ppkcfa"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -518,7 +522,6 @@ exports[`DocumentsTableComponent should render documents and partition keys in h
|
|||||||
noNativeElements={true}
|
noNativeElements={true}
|
||||||
role="grid"
|
role="grid"
|
||||||
size="small"
|
size="small"
|
||||||
sortable={true}
|
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"minWidth": "fit-content",
|
"minWidth": "fit-content",
|
||||||
@@ -536,9 +539,11 @@ exports[`DocumentsTableComponent should render documents and partition keys in h
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TableHeader>
|
<TableHeader
|
||||||
|
className="___1gzszts_0000000 f22iagw"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="fui-TableHeader ___oeyxrt0_1baslyg ftgm304"
|
className="fui-TableHeader ___1gzszts_39qb7g0 f22iagw"
|
||||||
role="rowgroup"
|
role="rowgroup"
|
||||||
>
|
>
|
||||||
<TableRow
|
<TableRow
|
||||||
@@ -601,6 +606,9 @@ exports[`DocumentsTableComponent should render documents and partition keys in h
|
|||||||
</TableSelectionCell>
|
</TableSelectionCell>
|
||||||
</div>
|
</div>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<div
|
||||||
|
className="___1ndi7nn_0000000 f64fuq3 f1ppkcfa"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ 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, WarningFilled } from "@fluentui/react-icons";
|
import { ErrorCircleFilled, MoreHorizontalRegular, QuestionRegular, 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,25 +34,32 @@ 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: () => null,
|
renderHeaderCell: () => "Code",
|
||||||
renderCell: (item) => item.code,
|
renderCell: (item) => <TableCellLayout truncate>{item.code}</TableCellLayout>,
|
||||||
}),
|
}),
|
||||||
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: () => null,
|
renderHeaderCell: () => "Severity",
|
||||||
renderCell: (item) => <TableCellLayout media={severityIcons[item.severity]}>{item.severity}</TableCellLayout>,
|
renderCell: (item) => (
|
||||||
|
<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) => (
|
||||||
item.location
|
<TableCellLayout truncate>
|
||||||
|
{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",
|
||||||
@@ -60,8 +67,20 @@ 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}>{item.message}</div>
|
<div className={styles.errorListMessage} title={item.message}>
|
||||||
<div>
|
{item.message}
|
||||||
|
</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"
|
||||||
@@ -76,9 +95,9 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
|
|||||||
|
|
||||||
const columnSizingOptions: TableColumnSizingOptions = {
|
const columnSizingOptions: TableColumnSizingOptions = {
|
||||||
code: {
|
code: {
|
||||||
minWidth: 75,
|
minWidth: 90,
|
||||||
idealWidth: 75,
|
idealWidth: 90,
|
||||||
defaultWidth: 75,
|
defaultWidth: 90,
|
||||||
},
|
},
|
||||||
severity: {
|
severity: {
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ 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";
|
||||||
@@ -16,6 +18,24 @@ 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(() => {
|
||||||
@@ -32,7 +52,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",
|
||||||
@@ -50,6 +70,17 @@ 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,9 +6,11 @@ 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";
|
||||||
@@ -46,7 +48,6 @@ 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";
|
||||||
@@ -54,7 +55,6 @@ 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,13 +209,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
|
|
||||||
private _queryCopilotActive(): boolean {
|
private _queryCopilotActive(): boolean {
|
||||||
if (this.props.copilotEnabled) {
|
if (this.props.copilotEnabled) {
|
||||||
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
return readCopilotToggleStatus(userContext.databaseAccount);
|
||||||
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
|
||||||
);
|
|
||||||
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
|
||||||
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
|
||||||
: true;
|
|
||||||
return copilotInitialActive;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -584,7 +578,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);
|
||||||
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, active.toString());
|
saveCopilotToggleStatus(userContext.databaseAccount, active);
|
||||||
|
|
||||||
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,6 +72,11 @@ 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",
|
||||||
@@ -80,5 +85,12 @@ export const useQueryTabStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
errorListMessage: {
|
errorListMessage: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
errorListMessageActions: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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";
|
||||||
@@ -15,7 +16,6 @@ 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,12 +1,13 @@
|
|||||||
import { IMessageBarStyles, Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
import { IMessageBarStyles, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext, updateConfigContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
import { IpRule } from "Contracts/DataModels";
|
import { IpRule } from "Contracts/DataModels";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
|
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";
|
||||||
@@ -16,7 +17,6 @@ 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,9 +37,6 @@ 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,
|
||||||
@@ -87,29 +84,6 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
{networkSettingsWarning}
|
{networkSettingsWarning}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
{showRUThresholdMessageBar && (
|
|
||||||
<MessageBar
|
|
||||||
messageBarType={MessageBarType.info}
|
|
||||||
onDismiss={() => {
|
|
||||||
setShowRUThresholdMessageBar(false);
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
...defaultMessageBarStyles,
|
|
||||||
innerText: {
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`Data Explorer has a 5,000 RU default limit. To adjust the limit, go to the Settings page 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}
|
||||||
@@ -118,7 +92,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{`We are moving our middleware to new infrastructure. To avoid future issues with Data Explorer access, please
|
{`We have migrated our middleware to new infrastructure. To avoid issues with Data Explorer access, please
|
||||||
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
@@ -133,6 +107,7 @@ 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} />
|
||||||
@@ -397,12 +372,6 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
|||||||
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ipRulesIncludeMongoProxy) {
|
|
||||||
updateConfigContext({
|
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return !ipRulesIncludeMongoProxy;
|
return !ipRulesIncludeMongoProxy;
|
||||||
} else if (userContext.apiType === "Cassandra") {
|
} else if (userContext.apiType === "Cassandra") {
|
||||||
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
||||||
@@ -421,12 +390,6 @@ const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
|||||||
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ipRulesIncludeCassandraProxy) {
|
|
||||||
updateConfigContext({
|
|
||||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return !ipRulesIncludeCassandraProxy;
|
return !ipRulesIncludeCassandraProxy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +10,6 @@ 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
|
||||||
@@ -28,7 +27,6 @@ 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;
|
||||||
|
|
||||||
@@ -45,7 +43,6 @@ 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,5 +1,6 @@
|
|||||||
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";
|
||||||
@@ -14,7 +15,6 @@ 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,5 +1,6 @@
|
|||||||
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";
|
||||||
@@ -13,7 +14,6 @@ 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 {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
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";
|
||||||
@@ -25,7 +24,6 @@ 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";
|
||||||
@@ -1020,41 +1018,6 @@ 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,8 +4,6 @@ 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";
|
||||||
@@ -76,7 +74,6 @@ 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;
|
||||||
@@ -87,10 +84,8 @@ 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
|
try {
|
||||||
(data: any) => {
|
|
||||||
const pendingNotification: DataModels.Notification = data?.[0];
|
|
||||||
const tabOptions: ViewModels.TabOptions = {
|
const tabOptions: ViewModels.TabOptions = {
|
||||||
tabKind,
|
tabKind,
|
||||||
title: "Scale",
|
title: "Scale",
|
||||||
@@ -101,10 +96,8 @@ export default class Database implements ViewModels.Database {
|
|||||||
onLoadStartKey: startKey,
|
onLoadStartKey: startKey,
|
||||||
};
|
};
|
||||||
settingsTab = new DatabaseSettingsTabV2(tabOptions);
|
settingsTab = new DatabaseSettingsTabV2(tabOptions);
|
||||||
settingsTab.pendingNotification(pendingNotification);
|
|
||||||
useTabs.getState().activateNewTab(settingsTab);
|
useTabs.getState().activateNewTab(settingsTab);
|
||||||
},
|
} catch (error) {
|
||||||
(error) => {
|
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
Action.Tab,
|
Action.Tab,
|
||||||
@@ -121,19 +114,9 @@ export default class Database implements ViewModels.Database {
|
|||||||
);
|
);
|
||||||
logConsoleError(`Error while fetching database settings for database ${this.id()}: ${errorMessage}`);
|
logConsoleError(`Error while fetching database settings for database ${this.id()}: ${errorMessage}`);
|
||||||
throw error;
|
throw error;
|
||||||
},
|
}
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
pendingNotificationsPromise.then(
|
|
||||||
(pendingNotification: DataModels.Notification) => {
|
|
||||||
settingsTab.pendingNotification(pendingNotification);
|
|
||||||
useTabs.getState().activateTab(settingsTab);
|
useTabs.getState().activateTab(settingsTab);
|
||||||
},
|
|
||||||
() => {
|
|
||||||
settingsTab.pendingNotification(undefined);
|
|
||||||
useTabs.getState().activateTab(settingsTab);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -260,42 +243,6 @@ 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,4 +1,6 @@
|
|||||||
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";
|
||||||
@@ -27,8 +29,6 @@ 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
|
||||||
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
||||||
},
|
},
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
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";
|
||||||
|
|||||||
@@ -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/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar";
|
||||||
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,5 +1,7 @@
|
|||||||
import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons";
|
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";
|
||||||
@@ -16,8 +18,6 @@ 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";
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.onDocumentDBDocumentsClick();
|
collection.onDocumentDBDocumentsClick();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
||||||
},
|
},
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
@@ -234,7 +234,7 @@ export const buildCollectionNode = (
|
|||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
||||||
},
|
},
|
||||||
onExpanded: async () => {
|
onExpanded: async () => {
|
||||||
// Rewritten version of expandCollapseCollection
|
// Rewritten version of expandCollapseCollection
|
||||||
@@ -282,7 +282,7 @@ const buildCollectionNodeChildren = (
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
collectionWasOpened(userContext.databaseAccount?.name, collection);
|
||||||
},
|
},
|
||||||
isSelected: () =>
|
isSelected: () =>
|
||||||
useSelectedNode
|
useSelectedNode
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ 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";
|
||||||
@@ -48,7 +50,6 @@ 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";
|
||||||
@@ -86,7 +87,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 */}
|
||||||
<CommandBar container={explorer} />
|
{!userContext.features.commandBarV2 && <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,22 +1,18 @@
|
|||||||
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">
|
||||||
<CommandButtonComponent
|
<div className="commandButtonReact">
|
||||||
id="commandbutton-feedback"
|
<a href="#" title="Send feedback" aria-haspopup="dialog" onClick={onClick}>
|
||||||
iconSrc={FeedbackIcon}
|
<img src={FeedbackIcon} alt="Send feedback" />
|
||||||
iconAlt="feeback button"
|
</a>
|
||||||
onCommandClick={() =>
|
</div>
|
||||||
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,6 +30,10 @@ 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,7 @@ export type Features = {
|
|||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
readonly enableDocumentsTableColumnSelection: boolean;
|
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 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
enableDocumentsTableColumnSelection: "true" === get("enabledocumentstablecolumnselection"),
|
commandBarV2: "true" === get("commandbarv2"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { createKeyFromPath, deleteState, loadState, MAX_ENTRY_NB, saveState } from "Shared/AppStatePersistenceUtility";
|
import {
|
||||||
|
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", () => ({
|
||||||
@@ -13,7 +21,7 @@ jest.mock("Shared/StorageUtility", () => ({
|
|||||||
|
|
||||||
describe("AppStatePersistenceUtility", () => {
|
describe("AppStatePersistenceUtility", () => {
|
||||||
const storePath = {
|
const storePath = {
|
||||||
componentName: "a",
|
componentName: AppStateComponentNames.DocumentsTab,
|
||||||
subComponentName: "b",
|
subComponentName: "b",
|
||||||
globalAccountName: "c",
|
globalAccountName: "c",
|
||||||
databaseName: "d",
|
databaseName: "d",
|
||||||
@@ -166,5 +174,27 @@ 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,8 +1,13 @@
|
|||||||
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 type ComponentName = "DocumentsTab";
|
export enum AppStateComponentNames {
|
||||||
|
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
|
||||||
@@ -14,8 +19,9 @@ export interface StateData {
|
|||||||
data: unknown;
|
data: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorePath = {
|
// Export for testing purposes
|
||||||
componentName: string;
|
export type StorePath = {
|
||||||
|
componentName: AppStateComponentNames;
|
||||||
subComponentName?: string;
|
subComponentName?: string;
|
||||||
globalAccountName?: string;
|
globalAccountName?: string;
|
||||||
databaseName?: string;
|
databaseName?: string;
|
||||||
@@ -29,6 +35,7 @@ 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 =
|
||||||
@@ -60,6 +67,10 @@ 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 => {
|
||||||
@@ -87,16 +98,10 @@ const orderedPathSegments: (keyof StorePath)[] = [
|
|||||||
* @param path
|
* @param path
|
||||||
*/
|
*/
|
||||||
export const createKeyFromPath = (path: StorePath): string => {
|
export const createKeyFromPath = (path: StorePath): string => {
|
||||||
if (path.componentName.includes("/")) {
|
let key = `${PATH_SEPARATOR}${encodeURIComponent(path.componentName)}`; // ComponentName is always there
|
||||||
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];
|
||||||
if (segmentValue.includes("/")) {
|
key += `${PATH_SEPARATOR}${segmentValue !== undefined ? encodeURIComponent(segmentValue) : ""}`;
|
||||||
throw new Error(`Invalid setting path segment: ${segment}`);
|
|
||||||
}
|
|
||||||
key += `/${segmentValue !== undefined ? segmentValue : ""}`;
|
|
||||||
});
|
});
|
||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export enum StorageKey {
|
|||||||
MaxDegreeOfParellism,
|
MaxDegreeOfParellism,
|
||||||
IsGraphAutoVizDisabled,
|
IsGraphAutoVizDisabled,
|
||||||
TenantId,
|
TenantId,
|
||||||
MostRecentActivity,
|
MostRecentActivity, // deprecated
|
||||||
SetPartitionKeyUndefined,
|
SetPartitionKeyUndefined,
|
||||||
GalleryCalloutDismissed,
|
GalleryCalloutDismissed,
|
||||||
VisitedAccounts,
|
VisitedAccounts,
|
||||||
|
|||||||
@@ -192,6 +192,11 @@ 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,7 +4,28 @@ 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 { extractPartitionKeyValues } from "./QueryUtils";
|
import { defaultQueryFields, extractPartitionKeyValues, getValueForPath } 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 => {
|
||||||
@@ -54,6 +75,20 @@ 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()", () => {
|
||||||
@@ -97,28 +132,30 @@ describe("Query Utils", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("extractPartitionKey", () => {
|
describe("getValueForPath", () => {
|
||||||
const documentContent = {
|
it("should return the correct value for a simple path", () => {
|
||||||
"Volcano Name": "Adams",
|
const pathSegments = ["Volcano Name"];
|
||||||
Country: "United States",
|
expect(getValueForPath(documentContent, pathSegments)).toBe("Adams");
|
||||||
Region: "US-Washington",
|
});
|
||||||
Location: {
|
it("should return the correct value for a nested path", () => {
|
||||||
type: "Point",
|
const pathSegments = ["Location", "coordinates"];
|
||||||
coordinates: [-121.49, 46.206],
|
expect(getValueForPath(documentContent, pathSegments)).toEqual([-121.49, 46.206]);
|
||||||
},
|
});
|
||||||
Elevation: 3742,
|
it("should return undefined for a non-existing path", () => {
|
||||||
Type: "Stratovolcano",
|
const pathSegments = ["NonExistent", "Path"];
|
||||||
Category: "",
|
expect(getValueForPath(documentContent, pathSegments)).toBeUndefined();
|
||||||
Status: "Tephrochronology",
|
});
|
||||||
"Last Known Eruption": "Last known eruption from A.D. 1-1499, inclusive",
|
it("should return undefined for an invalid path", () => {
|
||||||
id: "9e3c494e-8367-3f50-1f56-8c6fcb961363",
|
const pathSegments = ["Location", "InvalidKey"];
|
||||||
_rid: "xzo0AJRYUxUFAAAAAAAAAA==",
|
expect(getValueForPath(documentContent, pathSegments)).toBeUndefined();
|
||||||
_self: "dbs/xzo0AA==/colls/xzo0AJRYUxU=/docs/xzo0AJRYUxUFAAAAAAAAAA==/",
|
});
|
||||||
_etag: '"ce00fa43-0000-0100-0000-652840440000"',
|
it("should return the root object if pathSegments is empty", () => {
|
||||||
_attachments: "attachments/",
|
const pathSegments: string[] = [];
|
||||||
_ts: 1697136708,
|
expect(getValueForPath(documentContent, pathSegments)).toBeUndefined();
|
||||||
};
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractPartitionKey", () => {
|
||||||
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,
|
||||||
@@ -175,5 +212,18 @@ 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"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ export function buildDocumentsQuery(
|
|||||||
additionalField: string[] = [],
|
additionalField: string[] = [],
|
||||||
): string {
|
): string {
|
||||||
const fieldSet = new Set<string>(defaultQueryFields);
|
const fieldSet = new Set<string>(defaultQueryFields);
|
||||||
additionalField.forEach((prop) => fieldSet.add(prop));
|
additionalField.forEach((prop) => {
|
||||||
|
if (!partitionKeyProperties.includes(prop)) {
|
||||||
|
fieldSet.add(prop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const objectListSpec = [...fieldSet]
|
const objectListSpec = [...fieldSet].map((prop) => `c.${prop}`).join(",");
|
||||||
.filter((f) => !partitionKeyProperties.includes(f))
|
|
||||||
.map((prop) => `c.${prop}`)
|
|
||||||
.join(",");
|
|
||||||
let query =
|
let query =
|
||||||
partitionKeyProperties && partitionKeyProperties.length > 0
|
partitionKeyProperties && partitionKeyProperties.length > 0
|
||||||
? `select ${objectListSpec}, [${buildDocumentsQueryPartitionProjections(
|
? `select ${objectListSpec}, [${buildDocumentsQueryPartitionProjections(
|
||||||
@@ -94,6 +95,24 @@ 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,
|
||||||
@@ -104,11 +123,15 @@ export const extractPartitionKeyValues = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const partitionKeyValues: PartitionKey[] = [];
|
const partitionKeyValues: PartitionKey[] = [];
|
||||||
|
|
||||||
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
|
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
|
||||||
const partitionKeyPathWithoutSlash: string = partitionKeyPath.substring(1);
|
const pathSegments: string[] = partitionKeyPath.substring(1).split("/");
|
||||||
if (documentContent[partitionKeyPathWithoutSlash] !== undefined) {
|
const value = getValueForPath(documentContent, pathSegments);
|
||||||
partitionKeyValues.push(documentContent[partitionKeyPathWithoutSlash]);
|
|
||||||
|
if (value !== undefined) {
|
||||||
|
partitionKeyValues.push(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return partitionKeyValues;
|
return partitionKeyValues;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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";
|
||||||
@@ -760,11 +761,17 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let url: string;
|
||||||
|
if (useNewPortalBackendEndpoint(Constants.BackendApi.SampleData)) {
|
||||||
|
url = createUri(configContext.PORTAL_BACKEND_ENDPOINT, "/api/sampledata");
|
||||||
|
} else {
|
||||||
const sampleDatabaseEndpoint = useQueryCopilot.getState().copilotUserDBEnabled
|
const sampleDatabaseEndpoint = useQueryCopilot.getState().copilotUserDBEnabled
|
||||||
? `/api/tokens/sampledataconnection/v2`
|
? `/api/tokens/sampledataconnection/v2`
|
||||||
: `/api/tokens/sampledataconnection`;
|
: `/api/tokens/sampledataconnection`;
|
||||||
|
|
||||||
const url = createUri(`${configContext.BACKEND_ENDPOINT}`, sampleDatabaseEndpoint);
|
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