mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-24 12:14:17 +00:00
Compare commits
19 Commits
copilot_re
...
users/aisa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a96f4bbb46 | ||
|
|
8075ef2847 | ||
|
|
063ad23bce | ||
|
|
eacbeae417 | ||
|
|
94158504a8 | ||
|
|
9b032ecae4 | ||
|
|
6493c985b4 | ||
|
|
569167fa10 | ||
|
|
6e267b2bba | ||
|
|
14d7677056 | ||
|
|
282004b09b | ||
|
|
d376a7463c | ||
|
|
9669301d14 | ||
|
|
dcd8d1637b | ||
|
|
f36fccd3ef | ||
|
|
8ff9a84004 | ||
|
|
e42e24b175 | ||
|
|
44d6f29edd | ||
|
|
9db06af552 |
@@ -1179,16 +1179,16 @@ menuQuickStart {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gridRowSelected {
|
#tbodycontent tr.gridRowSelected {
|
||||||
.active();
|
.active();
|
||||||
}
|
}
|
||||||
|
|
||||||
.gridRowSelected:hover {
|
#tbodycontent tr.gridRowSelected:hover {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
.hover();
|
.hover();
|
||||||
}
|
}
|
||||||
|
|
||||||
.gridRowHighlighted {
|
#tbodycontent tr.gridRowHighlighted {
|
||||||
border-style: dotted;
|
border-style: dotted;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
@@ -2576,9 +2576,10 @@ a:link {
|
|||||||
.querydropdown.placeholderVisible {
|
.querydropdown.placeholderVisible {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.querydropdown.placeholderVisible::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
.querydropdown.placeholderVisible::placeholder {
|
||||||
|
/* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||||
color: #767474;
|
color: #767474;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.querydropdown:hover {
|
.querydropdown:hover {
|
||||||
@@ -2648,7 +2649,7 @@ a:link {
|
|||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
border-bottom: 2px solid rgba(0,120,212,1);
|
border-bottom: 2px solid rgba(0, 120, 212, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active:focus > .tabNavContentContainer {
|
.nav-tabs > li.active:focus > .tabNavContentContainer {
|
||||||
@@ -3096,4 +3097,3 @@ a:link {
|
|||||||
background: white;
|
background: white;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -179,10 +179,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@azure/cosmos": {
|
"@azure/cosmos": {
|
||||||
"version": "3.16.2",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.16.2.tgz",
|
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/@azure/cosmos/-/cosmos-4.0.0.tgz",
|
||||||
"integrity": "sha512-sceY5LWj0BHGj8PSyaVCfDRQLVZyoCfIY78kyIROJVEw0k+p9XFs8fhpykN8JklkCftL0WlaVY+X25SQwnhZsw==",
|
"integrity": "sha1-X9qLNctiu82lIVm5bEw5gahD1bk=",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"@azure/abort-controller": "^1.0.0",
|
||||||
"@azure/core-auth": "^1.3.0",
|
"@azure/core-auth": "^1.3.0",
|
||||||
"@azure/core-rest-pipeline": "^1.2.0",
|
"@azure/core-rest-pipeline": "^1.2.0",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
@@ -22133,6 +22134,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz",
|
||||||
"integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA=="
|
"integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA=="
|
||||||
},
|
},
|
||||||
|
"react-string-format": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/react-string-format/-/react-string-format-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-JyQaRZHqURInBBx64HC3FJBh3AA=",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"react-syntax-highlighter": {
|
"react-syntax-highlighter": {
|
||||||
"version": "12.2.1",
|
"version": "12.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "3.16.2",
|
"@azure/cosmos": "4.0.0",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "1.2.1",
|
"@azure/identity": "1.2.1",
|
||||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
"@azure/ms-rest-nodeauth": "3.0.7",
|
||||||
@@ -92,6 +92,7 @@
|
|||||||
"react-notification-system": "0.2.17",
|
"react-notification-system": "0.2.17",
|
||||||
"react-redux": "7.1.3",
|
"react-redux": "7.1.3",
|
||||||
"react-splitter-layout": "4.0.0",
|
"react-splitter-layout": "4.0.0",
|
||||||
|
"react-string-format": "1.0.1",
|
||||||
"react-youtube": "9.0.1",
|
"react-youtube": "9.0.1",
|
||||||
"redux": "4.0.4",
|
"redux": "4.0.4",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
@@ -234,4 +235,4 @@
|
|||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -427,12 +427,6 @@ export class JunoEndpoints {
|
|||||||
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PriorityLevel {
|
|
||||||
public static readonly High = "high";
|
|
||||||
public static readonly Low = "low";
|
|
||||||
public static readonly Default = "low";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
|
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
|
||||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ describe("requestPlugin", () => {
|
|||||||
const headers = {};
|
const headers = {};
|
||||||
const endpoint = "https://docs.azure.com";
|
const endpoint = "https://docs.azure.com";
|
||||||
const path = "/dbs/foo";
|
const path = "/dbs/foo";
|
||||||
requestPlugin({ endpoint, headers, path } as any, next as any);
|
requestPlugin({ endpoint, headers, path } as any, undefined, next as any);
|
||||||
expect(next.mock.calls[0][0]).toMatchSnapshot();
|
expect(next.mock.calls[0][0]).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -137,7 +137,7 @@ describe("requestPlugin", () => {
|
|||||||
const headers = {};
|
const headers = {};
|
||||||
const endpoint = "";
|
const endpoint = "";
|
||||||
const path = "/dbs/foo";
|
const path = "/dbs/foo";
|
||||||
requestPlugin({ endpoint, headers, path } as any, next as any);
|
requestPlugin({ endpoint, headers, path } as any, undefined, next as any);
|
||||||
expect(next.mock.calls[0][0]).toMatchSnapshot();
|
expect(next.mock.calls[0][0]).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
import { configContext, Platform } from "../ConfigContext";
|
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||||
|
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
|
||||||
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
import { getErrorMessage } from "./ErrorHandlingUtils";
|
import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
|
||||||
import { AuthType } from "../AuthType";
|
|
||||||
|
|
||||||
const _global = typeof self === "undefined" ? window : self;
|
const _global = typeof self === "undefined" ? window : self;
|
||||||
|
|
||||||
@@ -26,6 +25,15 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (configContext.platform === Platform.Fabric) {
|
||||||
|
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [
|
||||||
|
requestInfo,
|
||||||
|
]);
|
||||||
|
console.log("Response from Fabric: ", authorizationToken);
|
||||||
|
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
||||||
|
return authorizationToken.PrimaryReadWriteToken;
|
||||||
|
}
|
||||||
|
|
||||||
if (userContext.masterKey) {
|
if (userContext.masterKey) {
|
||||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||||
@@ -41,7 +49,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
return decodeURIComponent(result.PrimaryReadWriteToken);
|
return decodeURIComponent(result.PrimaryReadWriteToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
|
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, diagnosticNode, next) => {
|
||||||
requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href;
|
requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href;
|
||||||
requestContext.headers["x-ms-proxy-target"] = endpoint();
|
requestContext.headers["x-ms-proxy-target"] = endpoint();
|
||||||
return next(requestContext);
|
return next(requestContext);
|
||||||
@@ -56,7 +64,11 @@ export const endpoint = () => {
|
|||||||
return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
|
return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> {
|
export async function getTokenFromAuthService(
|
||||||
|
verb: string,
|
||||||
|
resourceType: string,
|
||||||
|
resourceId?: string,
|
||||||
|
): Promise<AuthorizationToken> {
|
||||||
try {
|
try {
|
||||||
const host = configContext.BACKEND_ENDPOINT;
|
const host = configContext.BACKEND_ENDPOINT;
|
||||||
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
||||||
@@ -95,18 +107,6 @@ export function client(): Cosmos.CosmosClient {
|
|||||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||||
|
|
||||||
if (
|
|
||||||
userContext.authType === AuthType.ConnectionString ||
|
|
||||||
userContext.authType === AuthType.EncryptedToken ||
|
|
||||||
userContext.authType === AuthType.ResourceToken
|
|
||||||
) {
|
|
||||||
// Default to low priority. Needed for non-AAD-auth scenarios
|
|
||||||
// where we cannot use RP API, and thus, cannot detect whether priority
|
|
||||||
// based execution is enabled.
|
|
||||||
// The header will be ignored if priority based execution is disabled on the account.
|
|
||||||
_defaultHeaders["x-ms-cosmos-priority-level"] = PriorityLevel.Default;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: Cosmos.CosmosClientOptions = {
|
const options: Cosmos.CosmosClientOptions = {
|
||||||
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||||
key: userContext.masterKey,
|
key: userContext.masterKey,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function handleCachedDataMessage(message: any): void {
|
|||||||
if (messageContent.error != null) {
|
if (messageContent.error != null) {
|
||||||
cachedDataPromise.deferred.reject(messageContent.error);
|
cachedDataPromise.deferred.reject(messageContent.error);
|
||||||
} else {
|
} else {
|
||||||
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
|
cachedDataPromise.deferred.resolve(messageContent.data);
|
||||||
}
|
}
|
||||||
runGarbageCollector();
|
runGarbageCollector();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,6 @@
|
|||||||
|
import { MessageTypes } from "Contracts/MessageTypes";
|
||||||
import * as ActionContracts from "./ActionContracts";
|
import * as ActionContracts from "./ActionContracts";
|
||||||
import * as Diagnostics from "./Diagnostics";
|
import * as Diagnostics from "./Diagnostics";
|
||||||
import * as Versions from "./Versions";
|
import * as Versions from "./Versions";
|
||||||
|
|
||||||
/**
|
export { ActionContracts, Diagnostics, MessageTypes, Versions };
|
||||||
* Messaging types used with Data Explorer <-> Portal communication
|
|
||||||
* and Hosted <-> Explorer communication
|
|
||||||
*/
|
|
||||||
export enum MessageTypes {
|
|
||||||
TelemetryInfo,
|
|
||||||
LogInfo,
|
|
||||||
RefreshResources,
|
|
||||||
AllDatabases,
|
|
||||||
CollectionsForDatabase,
|
|
||||||
RefreshOffers,
|
|
||||||
AllOffers,
|
|
||||||
UpdateLocationHash,
|
|
||||||
SingleOffer,
|
|
||||||
RefreshOffer,
|
|
||||||
UpdateAccountName,
|
|
||||||
ForbiddenError,
|
|
||||||
AadSignIn,
|
|
||||||
GetAccessAadRequest,
|
|
||||||
GetAccessAadResponse,
|
|
||||||
UpdateAccountSwitch,
|
|
||||||
UpdateDirectoryControl,
|
|
||||||
SwitchAccount,
|
|
||||||
SendNotification,
|
|
||||||
ClearNotification,
|
|
||||||
ExplorerClickEvent,
|
|
||||||
LoadingStatus,
|
|
||||||
GetArcadiaToken,
|
|
||||||
CreateWorkspace,
|
|
||||||
CreateSparkPool,
|
|
||||||
RefreshDatabaseAccount,
|
|
||||||
CloseTab,
|
|
||||||
OpenQuickstartBlade,
|
|
||||||
OpenPostgreSQLPasswordReset,
|
|
||||||
OpenPostgresNetworkingBlade,
|
|
||||||
OpenCosmosDBNetworkingBlade,
|
|
||||||
DisplayNPSSurvey,
|
|
||||||
OpenVCoreMongoNetworkingBlade,
|
|
||||||
OpenVCoreMongoConnectionStringsBlade,
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ActionContracts, Diagnostics, Versions };
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { AuthorizationToken, MessageTypes } from "./MessageTypes";
|
||||||
|
|
||||||
export type FabricMessage =
|
export type FabricMessage =
|
||||||
| {
|
| {
|
||||||
type: "newContainer";
|
type: "newContainer";
|
||||||
@@ -5,21 +7,52 @@ export type FabricMessage =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "initialize";
|
type: "initialize";
|
||||||
connectionString: string | undefined;
|
message: {
|
||||||
|
endpoint: string | undefined;
|
||||||
|
error: string | undefined;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "openTab";
|
type: "openTab";
|
||||||
databaseName: string;
|
databaseName: string;
|
||||||
collectionName: string | undefined;
|
collectionName: string | undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "authorizationToken";
|
||||||
|
message: {
|
||||||
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
|
data: AuthorizationToken | undefined;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataExploreMessage =
|
export type DataExploreMessage =
|
||||||
| "ready"
|
| "ready"
|
||||||
| {
|
| {
|
||||||
type: number;
|
type: MessageTypes.TelemetryInfo;
|
||||||
data: {
|
data: {
|
||||||
action: "LoadDatabases";
|
action: "LoadDatabases";
|
||||||
actionModifier: "success" | "start";
|
actionModifier: "success" | "start";
|
||||||
defaultExperience: "SQL";
|
defaultExperience: "SQL";
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MessageTypes.GetAuthorizationToken;
|
||||||
|
id: string;
|
||||||
|
params: GetCosmosTokenMessageOptions[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetCosmosTokenMessageOptions = {
|
||||||
|
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
||||||
|
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
|
||||||
|
resourceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CosmosDBTokenResponse = {
|
||||||
|
token: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CosmosDBConnectionInfoResponse = {
|
||||||
|
endpoint: string;
|
||||||
|
};
|
||||||
|
|||||||
48
src/Contracts/MessageTypes.ts
Normal file
48
src/Contracts/MessageTypes.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Messaging types used with Data Explorer <-> Portal communication,
|
||||||
|
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
|
||||||
|
*/
|
||||||
|
export enum MessageTypes {
|
||||||
|
TelemetryInfo,
|
||||||
|
LogInfo,
|
||||||
|
RefreshResources,
|
||||||
|
AllDatabases,
|
||||||
|
CollectionsForDatabase,
|
||||||
|
RefreshOffers,
|
||||||
|
AllOffers,
|
||||||
|
UpdateLocationHash,
|
||||||
|
SingleOffer,
|
||||||
|
RefreshOffer,
|
||||||
|
UpdateAccountName,
|
||||||
|
ForbiddenError,
|
||||||
|
AadSignIn,
|
||||||
|
GetAccessAadRequest,
|
||||||
|
GetAccessAadResponse,
|
||||||
|
UpdateAccountSwitch,
|
||||||
|
UpdateDirectoryControl,
|
||||||
|
SwitchAccount,
|
||||||
|
SendNotification,
|
||||||
|
ClearNotification,
|
||||||
|
ExplorerClickEvent,
|
||||||
|
LoadingStatus,
|
||||||
|
GetArcadiaToken,
|
||||||
|
CreateWorkspace,
|
||||||
|
CreateSparkPool,
|
||||||
|
RefreshDatabaseAccount,
|
||||||
|
CloseTab,
|
||||||
|
OpenQuickstartBlade,
|
||||||
|
OpenPostgreSQLPasswordReset,
|
||||||
|
OpenPostgresNetworkingBlade,
|
||||||
|
OpenCosmosDBNetworkingBlade,
|
||||||
|
DisplayNPSSurvey,
|
||||||
|
OpenVCoreMongoNetworkingBlade,
|
||||||
|
OpenVCoreMongoConnectionStringsBlade,
|
||||||
|
|
||||||
|
// Data Explorer -> Fabric communication
|
||||||
|
GetAuthorizationToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizationToken {
|
||||||
|
XDate: string;
|
||||||
|
PrimaryReadWriteToken: string;
|
||||||
|
}
|
||||||
@@ -123,19 +123,6 @@ describe("ContainerSampleGenerator", () => {
|
|||||||
await generator.createSampleContainerAsync();
|
await generator.createSampleContainerAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not create any sample for Mongo API account", async () => {
|
|
||||||
const experience = "Sample generation not supported for this API Mongo";
|
|
||||||
updateUserContext({
|
|
||||||
databaseAccount: {
|
|
||||||
properties: {
|
|
||||||
capabilities: [{ name: "EnableMongo" }],
|
|
||||||
},
|
|
||||||
} as DatabaseAccount,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not create any sample for Table API account", async () => {
|
it("should not create any sample for Table API account", async () => {
|
||||||
const experience = "Sample generation not supported for this API Tables";
|
const experience = "Sample generation not supported for this API Tables";
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -689,9 +689,9 @@ export default class Explorer {
|
|||||||
private _initSettings() {
|
private _initSettings() {
|
||||||
if (!ExplorerSettings.hasSettingsDefined()) {
|
if (!ExplorerSettings.hasSettingsDefined()) {
|
||||||
ExplorerSettings.createDefaultSettings();
|
ExplorerSettings.createDefaultSettings();
|
||||||
} else {
|
|
||||||
ExplorerSettings.ensurePriorityLevel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExplorerSettings.ensurePriorityLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public uploadFile(
|
public uploadFile(
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.apiType !== "Tables") {
|
if (userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric) {
|
||||||
newCollectionBtn.children = [createNewCollectionGroup(container)];
|
newCollectionBtn.children = [createNewCollectionGroup(container)];
|
||||||
const newDatabaseBtn = createNewDatabase(container);
|
const newDatabaseBtn = createNewDatabase(container);
|
||||||
newCollectionBtn.children.push(newDatabaseBtn);
|
newCollectionBtn.children.push(newDatabaseBtn);
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
|
createNewDatabase:
|
||||||
|
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
||||||
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
||||||
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
||||||
selectedDatabaseId:
|
selectedDatabaseId:
|
||||||
@@ -274,36 +275,38 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
{configContext.platform !== Platform.Fabric && (
|
||||||
<div role="radiogroup">
|
<Stack horizontal verticalAlign="center">
|
||||||
<input
|
<div role="radiogroup">
|
||||||
className="panelRadioBtn"
|
<input
|
||||||
checked={this.state.createNewDatabase}
|
className="panelRadioBtn"
|
||||||
aria-label="Create new database"
|
checked={this.state.createNewDatabase}
|
||||||
aria-checked={this.state.createNewDatabase}
|
aria-label="Create new database"
|
||||||
name="databaseType"
|
aria-checked={this.state.createNewDatabase}
|
||||||
type="radio"
|
name="databaseType"
|
||||||
role="radio"
|
type="radio"
|
||||||
id="databaseCreateNew"
|
role="radio"
|
||||||
tabIndex={0}
|
id="databaseCreateNew"
|
||||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
tabIndex={0}
|
||||||
/>
|
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||||
<span className="panelRadioBtnLabel">Create new</span>
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">Create new</span>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={!this.state.createNewDatabase}
|
checked={!this.state.createNewDatabase}
|
||||||
aria-label="Use existing database"
|
aria-label="Use existing database"
|
||||||
aria-checked={!this.state.createNewDatabase}
|
aria-checked={!this.state.createNewDatabase}
|
||||||
name="databaseType"
|
name="databaseType"
|
||||||
type="radio"
|
type="radio"
|
||||||
role="radio"
|
role="radio"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||||
/>
|
/>
|
||||||
<span className="panelRadioBtnLabel">Use existing</span>
|
<span className="panelRadioBtnLabel">Use existing</span>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
{this.state.createNewDatabase && (
|
{this.state.createNewDatabase && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
|
import { PriorityLevel } from "@azure/cosmos";
|
||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
ChoiceGroup,
|
||||||
|
IChoiceGroupOption,
|
||||||
|
ISpinButtonStyles,
|
||||||
|
IToggleStyles,
|
||||||
|
Position,
|
||||||
|
SpinButton,
|
||||||
|
Toggle,
|
||||||
|
} from "@fluentui/react";
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
import { configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
@@ -6,10 +16,10 @@ import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
|||||||
import * as StringUtility from "Shared/StringUtility";
|
import * as StringUtility from "Shared/StringUtility";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import React, { FunctionComponent, MouseEvent, useState } from "react";
|
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
|
||||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
|
|
||||||
export const SettingsPane: FunctionComponent = () => {
|
export const SettingsPane: FunctionComponent = () => {
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
@@ -19,6 +29,13 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
? Constants.Queries.UnlimitedPageOption
|
? Constants.Queries.UnlimitedPageOption
|
||||||
: Constants.Queries.CustomPageOption,
|
: Constants.Queries.CustomPageOption,
|
||||||
);
|
);
|
||||||
|
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
|
||||||
|
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
||||||
|
);
|
||||||
|
const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout));
|
||||||
|
const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>(
|
||||||
|
LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout),
|
||||||
|
);
|
||||||
const [customItemPerPage, setCustomItemPerPage] = useState<number>(
|
const [customItemPerPage, setCustomItemPerPage] = useState<number>(
|
||||||
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0,
|
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0,
|
||||||
);
|
);
|
||||||
@@ -42,10 +59,10 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
|
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
|
||||||
: Constants.Queries.DefaultMaxDegreeOfParallelism,
|
: Constants.Queries.DefaultMaxDegreeOfParallelism,
|
||||||
);
|
);
|
||||||
const [priorityLevel, setPriorityLevel] = useState<string>(
|
const [priorityLevel, setPriorityLevel] = useState<PriorityLevel>(
|
||||||
LocalStorageUtility.hasItem(StorageKey.PriorityLevel)
|
LocalStorageUtility.hasItem(StorageKey.PriorityLevel)
|
||||||
? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)
|
? (LocalStorageUtility.getEntryString(StorageKey.PriorityLevel) as PriorityLevel)
|
||||||
: Constants.PriorityLevel.Default,
|
: PriorityLevel.Low,
|
||||||
);
|
);
|
||||||
const explorerVersion = configContext.gitSha;
|
const explorerVersion = configContext.gitSha;
|
||||||
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
||||||
@@ -53,7 +70,7 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
||||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
||||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
||||||
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
|
const handlerOnSubmit = () => {
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
LocalStorageUtility.setEntryNumber(
|
LocalStorageUtility.setEntryNumber(
|
||||||
@@ -61,10 +78,11 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
||||||
);
|
);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||||
|
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
||||||
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
|
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
|
||||||
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
||||||
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
|
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel);
|
||||||
|
|
||||||
if (shouldShowGraphAutoVizOption) {
|
if (shouldShowGraphAutoVizOption) {
|
||||||
LocalStorageUtility.setEntryBoolean(
|
LocalStorageUtility.setEntryBoolean(
|
||||||
@@ -73,6 +91,14 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (queryTimeoutEnabled) {
|
||||||
|
LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout);
|
||||||
|
LocalStorageUtility.setEntryBoolean(
|
||||||
|
StorageKey.AutomaticallyCancelQueryAfterTimeout,
|
||||||
|
automaticallyCancelQueryAfterTimeout,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setIsExecuting(false);
|
setIsExecuting(false);
|
||||||
logConsoleInfo(
|
logConsoleInfo(
|
||||||
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
|
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
|
||||||
@@ -97,7 +123,6 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
||||||
);
|
);
|
||||||
closeSidePanel();
|
closeSidePanel();
|
||||||
e.preventDefault();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCustomPageOptionSelected = () => {
|
const isCustomPageOptionSelected = () => {
|
||||||
@@ -112,7 +137,7 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
formError: "",
|
formError: "",
|
||||||
isExecuting,
|
isExecuting,
|
||||||
submitButtonText: "Apply",
|
submitButtonText: "Apply",
|
||||||
onSubmit: () => handlerOnSubmit(undefined),
|
onSubmit: () => handlerOnSubmit(),
|
||||||
};
|
};
|
||||||
const pageOptionList: IChoiceGroupOption[] = [
|
const pageOptionList: IChoiceGroupOption[] = [
|
||||||
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
|
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
|
||||||
@@ -125,21 +150,36 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const priorityLevelOptionList: IChoiceGroupOption[] = [
|
const priorityLevelOptionList: IChoiceGroupOption[] = [
|
||||||
{ key: Constants.PriorityLevel.Low, text: "Low" },
|
{ key: PriorityLevel.Low, text: "Low" },
|
||||||
{ key: Constants.PriorityLevel.High, text: "High" },
|
{ key: PriorityLevel.High, text: "High" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleOnPriorityLevelOptionChange = (
|
const handleOnPriorityLevelOptionChange = (
|
||||||
ev: React.FormEvent<HTMLInputElement>,
|
ev: React.FormEvent<HTMLInputElement>,
|
||||||
option: IChoiceGroupOption,
|
option: IChoiceGroupOption,
|
||||||
): void => {
|
): void => {
|
||||||
setPriorityLevel(option.key);
|
setPriorityLevel(option.key as PriorityLevel);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
|
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
|
||||||
setPageOption(option.key);
|
setPageOption(option.key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
||||||
|
setQueryTimeoutEnabled(checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnAutomaticallyCancelQueryToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
||||||
|
setAutomaticallyCancelQueryAfterTimeout(checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnQueryTimeoutSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
|
||||||
|
const queryTimeout = Number(newValue);
|
||||||
|
if (!isNaN(queryTimeout)) {
|
||||||
|
setQueryTimeout(queryTimeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const choiceButtonStyles = {
|
const choiceButtonStyles = {
|
||||||
root: {
|
root: {
|
||||||
clear: "both",
|
clear: "both",
|
||||||
@@ -161,6 +201,35 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryTimeoutToggleStyles: IToggleStyles = {
|
||||||
|
label: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
display: "block",
|
||||||
|
},
|
||||||
|
root: {},
|
||||||
|
container: {},
|
||||||
|
pill: {},
|
||||||
|
thumb: {},
|
||||||
|
text: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryTimeoutSpinButtonStyles: ISpinButtonStyles = {
|
||||||
|
label: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
paddingBottom: 10,
|
||||||
|
},
|
||||||
|
labelWrapper: {},
|
||||||
|
icon: {},
|
||||||
|
spinButtonWrapper: {},
|
||||||
|
input: {},
|
||||||
|
arrowButtonsContainer: {},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...genericPaneProps}>
|
<RightPaneForm {...genericPaneProps}>
|
||||||
<div className="paneMainContent">
|
<div className="paneMainContent">
|
||||||
@@ -211,6 +280,50 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{userContext.apiType === "SQL" && (
|
||||||
|
<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
|
||||||
|
unless automatic cancellation has been enabled
|
||||||
|
</InfoTooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
styles={queryTimeoutToggleStyles}
|
||||||
|
label="Enable query timeout"
|
||||||
|
onChange={handleOnQueryTimeoutToggleChange}
|
||||||
|
defaultChecked={queryTimeoutEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{queryTimeoutEnabled && (
|
||||||
|
<div>
|
||||||
|
<SpinButton
|
||||||
|
label="Query timeout (ms)"
|
||||||
|
labelPosition={Position.top}
|
||||||
|
defaultValue={(queryTimeout || 5000).toString()}
|
||||||
|
min={100}
|
||||||
|
step={1000}
|
||||||
|
onChange={handleOnQueryTimeoutSpinButtonChange}
|
||||||
|
incrementButtonAriaLabel="Increase value by 1000"
|
||||||
|
decrementButtonAriaLabel="Decrease value by 1000"
|
||||||
|
styles={queryTimeoutSpinButtonStyles}
|
||||||
|
/>
|
||||||
|
<Toggle
|
||||||
|
label="Automatically cancel query after timeout"
|
||||||
|
styles={queryTimeoutToggleStyles}
|
||||||
|
onChange={handleOnAutomaticallyCancelQueryToggleChange}
|
||||||
|
defaultChecked={automaticallyCancelQueryAfterTimeout}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart">
|
<div className="settingsSectionPart">
|
||||||
<div className="settingsSectionLabel">
|
<div className="settingsSectionLabel">
|
||||||
@@ -289,8 +402,7 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
</legend>
|
</legend>
|
||||||
<InfoTooltip>
|
<InfoTooltip>
|
||||||
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.
|
||||||
server-side default priority level will be used.
|
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
<ChoiceGroup
|
<ChoiceGroup
|
||||||
ariaLabelledBy="priorityLevel"
|
ariaLabelledBy="priorityLevel"
|
||||||
|
|||||||
@@ -97,6 +97,46 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="settingsSection"
|
||||||
|
>
|
||||||
|
<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={
|
||||||
|
Object {
|
||||||
|
"container": Object {},
|
||||||
|
"label": Object {
|
||||||
|
"display": "block",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 400,
|
||||||
|
},
|
||||||
|
"pill": Object {},
|
||||||
|
"root": Object {},
|
||||||
|
"text": Object {},
|
||||||
|
"thumb": Object {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should submit submission", () => {
|
it("should not submit submission if required description field is null", () => {
|
||||||
const explorer = new Explorer();
|
const explorer = new Explorer();
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
|
||||||
|
|
||||||
@@ -110,12 +110,24 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
|||||||
submitButton.simulate("click");
|
submitButton.simulate("click");
|
||||||
wrapper.setProps({});
|
wrapper.setProps({});
|
||||||
|
|
||||||
|
expect(SubmitFeedback).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should submit submission", () => {
|
||||||
|
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
|
||||||
|
const explorer = new Explorer();
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
|
||||||
|
|
||||||
|
const submitButton = wrapper.find("form");
|
||||||
|
submitButton.simulate("submit");
|
||||||
|
wrapper.setProps({});
|
||||||
|
|
||||||
expect(SubmitFeedback).toHaveBeenCalledTimes(1);
|
expect(SubmitFeedback).toHaveBeenCalledTimes(1);
|
||||||
expect(SubmitFeedback).toHaveBeenCalledWith({
|
expect(SubmitFeedback).toHaveBeenCalledWith({
|
||||||
params: {
|
params: {
|
||||||
likeQuery: false,
|
likeQuery: false,
|
||||||
generatedQuery: "",
|
generatedQuery: "test query",
|
||||||
userPrompt: "",
|
userPrompt: "test prompt",
|
||||||
description: "",
|
description: "",
|
||||||
contact: getUserEmail(),
|
contact: getUserEmail(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,93 +25,94 @@ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }):
|
|||||||
closeFeedbackModal,
|
closeFeedbackModal,
|
||||||
setHideFeedbackModalForLikedQueries,
|
setHideFeedbackModalForLikedQueries,
|
||||||
} = useQueryCopilot();
|
} = useQueryCopilot();
|
||||||
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(true);
|
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(false);
|
||||||
const [description, setDescription] = React.useState<string>("");
|
const [description, setDescription] = React.useState<string>("");
|
||||||
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
||||||
const [contact, setContact] = React.useState<string>(getUserEmail());
|
const [contact, setContact] = React.useState<string>(getUserEmail());
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
closeFeedbackModal();
|
||||||
|
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
||||||
|
SubmitFeedback({
|
||||||
|
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
||||||
|
explorer: explorer,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={showFeedbackModal}>
|
<Modal isOpen={showFeedbackModal}>
|
||||||
<Stack style={{ padding: 24 }}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Stack horizontal horizontalAlign="space-between">
|
<Stack style={{ padding: 24 }}>
|
||||||
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
|
<Stack horizontal horizontalAlign="space-between">
|
||||||
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
|
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
|
||||||
</Stack>
|
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
|
||||||
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
|
</Stack>
|
||||||
<TextField
|
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
|
||||||
styles={{ root: { marginBottom: 14 } }}
|
<TextField
|
||||||
label="Description"
|
styles={{ root: { marginBottom: 14 } }}
|
||||||
required
|
label="Description"
|
||||||
placeholder="Provide more details"
|
required
|
||||||
value={description}
|
placeholder="Provide more details"
|
||||||
onChange={(_, newValue) => setDescription(newValue)}
|
value={description}
|
||||||
multiline
|
onChange={(_, newValue) => setDescription(newValue)}
|
||||||
rows={3}
|
multiline
|
||||||
/>
|
rows={3}
|
||||||
<TextField
|
|
||||||
styles={{ root: { marginBottom: 14 } }}
|
|
||||||
label="Query generated"
|
|
||||||
defaultValue={generatedQuery}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<ChoiceGroup
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
marginBottom: 14,
|
|
||||||
},
|
|
||||||
flexContainer: {
|
|
||||||
selectors: {
|
|
||||||
".ms-ChoiceField-field::before": { marginTop: 4 },
|
|
||||||
".ms-ChoiceField-field::after": { marginTop: 4 },
|
|
||||||
".ms-ChoiceFieldLabel": { paddingLeft: 6 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
label="May we contact you about your feedback?"
|
|
||||||
options={[
|
|
||||||
{ key: "yes", text: "Yes, you may contact me." },
|
|
||||||
{ key: "no", text: "No, do not contact me." },
|
|
||||||
]}
|
|
||||||
selectedKey={isContactAllowed ? "yes" : "no"}
|
|
||||||
onChange={(_, option) => {
|
|
||||||
setIsContactAllowed(option.key === "yes");
|
|
||||||
setContact(option.key === "yes" ? getUserEmail() : "");
|
|
||||||
}}
|
|
||||||
></ChoiceGroup>
|
|
||||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
|
|
||||||
{
|
|
||||||
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
|
|
||||||
Privacy statement
|
|
||||||
</Link>
|
|
||||||
}{" "}
|
|
||||||
for more information.
|
|
||||||
</Text>
|
|
||||||
{likeQuery && (
|
|
||||||
<Checkbox
|
|
||||||
styles={{ label: { paddingLeft: 0 }, root: { marginBottom: 14 } }}
|
|
||||||
label="Don't show me this next time"
|
|
||||||
checked={doNotShowAgainChecked}
|
|
||||||
onChange={(_, checked) => setDoNotShowAgainChecked(checked)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<TextField
|
||||||
<Stack horizontal horizontalAlign="end">
|
styles={{ root: { marginBottom: 14 } }}
|
||||||
<PrimaryButton
|
label="Query generated"
|
||||||
styles={{ root: { marginRight: 8 } }}
|
defaultValue={generatedQuery}
|
||||||
onClick={() => {
|
readOnly
|
||||||
closeFeedbackModal();
|
/>
|
||||||
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
<ChoiceGroup
|
||||||
SubmitFeedback({
|
styles={{
|
||||||
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
root: {
|
||||||
explorer: explorer,
|
marginBottom: 14,
|
||||||
});
|
},
|
||||||
|
flexContainer: {
|
||||||
|
selectors: {
|
||||||
|
".ms-ChoiceField-field::before": { marginTop: 4 },
|
||||||
|
".ms-ChoiceField-field::after": { marginTop: 4 },
|
||||||
|
".ms-ChoiceFieldLabel": { paddingLeft: 6 },
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
label="May we contact you about your feedback?"
|
||||||
Submit
|
options={[
|
||||||
</PrimaryButton>
|
{ key: "yes", text: "Yes, you may contact me." },
|
||||||
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
|
{ key: "no", text: "No, do not contact me." },
|
||||||
|
]}
|
||||||
|
selectedKey={isContactAllowed ? "yes" : "no"}
|
||||||
|
onChange={(_, option) => {
|
||||||
|
setIsContactAllowed(option.key === "yes");
|
||||||
|
setContact(option.key === "yes" ? getUserEmail() : "");
|
||||||
|
}}
|
||||||
|
></ChoiceGroup>
|
||||||
|
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||||
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
|
||||||
|
{
|
||||||
|
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
|
||||||
|
Privacy statement
|
||||||
|
</Link>
|
||||||
|
}{" "}
|
||||||
|
for more information.
|
||||||
|
</Text>
|
||||||
|
{likeQuery && (
|
||||||
|
<Checkbox
|
||||||
|
styles={{ label: { paddingLeft: 0 }, root: { marginBottom: 14 } }}
|
||||||
|
label="Don't show me this next time"
|
||||||
|
checked={doNotShowAgainChecked}
|
||||||
|
onChange={(_, checked) => setDoNotShowAgainChecked(checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Stack horizontal horizontalAlign="end">
|
||||||
|
<PrimaryButton styles={{ root: { marginRight: 8 } }} type="submit">
|
||||||
|
Submit
|
||||||
|
</PrimaryButton>
|
||||||
|
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import {
|
import {
|
||||||
Callout,
|
Callout,
|
||||||
CommandBarButton,
|
CommandBarButton,
|
||||||
@@ -141,7 +140,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
|
|
||||||
// Filter suggested prompts
|
// Filter suggested prompts
|
||||||
const filteredSuggested = suggestedPrompts.filter((prompt) =>
|
const filteredSuggested = suggestedPrompts.filter((prompt) =>
|
||||||
prompt.text.toLowerCase().includes(value.toLowerCase())
|
prompt.text.toLowerCase().includes(value.toLowerCase()),
|
||||||
);
|
);
|
||||||
setFilteredSuggestedPrompts(filteredSuggested);
|
setFilteredSuggestedPrompts(filteredSuggested);
|
||||||
};
|
};
|
||||||
@@ -151,7 +150,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
|
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
|
||||||
|
|
||||||
const updatedHistories = existingHistories.filter(
|
const updatedHistories = existingHistories.filter(
|
||||||
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase()
|
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase(),
|
||||||
);
|
);
|
||||||
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||||
|
|
||||||
@@ -238,7 +237,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
const showTeachingBubble = (): void => {
|
const showTeachingBubble = (): void => {
|
||||||
if (!inputEdited.current) {
|
if (!inputEdited.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!inputEdited.current) {
|
if (!inputEdited.current && !isWelcomModalVisible()) {
|
||||||
toggleCopilotTeachingBubbleVisible();
|
toggleCopilotTeachingBubbleVisible();
|
||||||
inputEdited.current = true;
|
inputEdited.current = true;
|
||||||
}
|
}
|
||||||
@@ -246,6 +245,10 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isWelcomModalVisible = (): boolean => {
|
||||||
|
return localStorage.getItem("hideWelcomeModal") !== "true";
|
||||||
|
};
|
||||||
|
|
||||||
const clearFeedback = () => {
|
const clearFeedback = () => {
|
||||||
resetButtonState();
|
resetButtonState();
|
||||||
resetQueryResults();
|
resetQueryResults();
|
||||||
@@ -298,7 +301,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
setShowSamplePrompts(true);
|
setShowSamplePrompts(true);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter" && userPrompt) {
|
||||||
inputEdited.current = true;
|
inputEdited.current = true;
|
||||||
startGenerateQueryProcess();
|
startGenerateQueryProcess();
|
||||||
}
|
}
|
||||||
@@ -534,7 +537,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
iconProps={{ iconName: "Copy" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||||
>
|
>
|
||||||
Copy code
|
Copy query
|
||||||
</CommandBarButton>
|
</CommandBarButton>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -543,11 +546,11 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
iconProps={{ iconName: "Delete" }}
|
iconProps={{ iconName: "Delete" }}
|
||||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||||
>
|
>
|
||||||
Delete code
|
Delete query
|
||||||
</CommandBarButton>
|
</CommandBarButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
<WelcomeModal visible={isWelcomModalVisible()} />
|
||||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||||
{query !== "" && query.trim().length !== 0 && (
|
{query !== "" && query.trim().length !== 0 && (
|
||||||
<DeletePopup
|
<DeletePopup
|
||||||
|
|||||||
@@ -21,8 +21,13 @@ 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 = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`);
|
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
||||||
const [copilotActive, setCopilotActive] = useState<boolean>(StringUtility.toBoolean(cachedCopilotToggleStatus));
|
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
||||||
|
);
|
||||||
|
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
||||||
|
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
||||||
|
: true;
|
||||||
|
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
|
||||||
|
|
||||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||||
@@ -87,7 +92,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
<QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar>
|
<QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar>
|
||||||
)}
|
)}
|
||||||
<Stack className="tabPaneContentContainer">
|
<Stack className="tabPaneContentContainer">
|
||||||
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
language={"sql"}
|
language={"sql"}
|
||||||
content={query}
|
content={query}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export const OnExecuteQueryClick = async (): Promise<void> => {
|
|||||||
|
|
||||||
export const QueryDocumentsPerPage = async (
|
export const QueryDocumentsPerPage = async (
|
||||||
firstItemIndex: number,
|
firstItemIndex: number,
|
||||||
queryIterator: MinimalQueryIterator
|
queryIterator: MinimalQueryIterator,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
useQueryCopilot.getState().setIsExecuting(true);
|
useQueryCopilot.getState().setIsExecuting(true);
|
||||||
@@ -174,7 +174,8 @@ export const QueryDocumentsPerPage = async (
|
|||||||
useTabs.getState().setIsQueryErrorThrown(false);
|
useTabs.getState().setIsQueryErrorThrown(false);
|
||||||
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
||||||
firstItemIndex,
|
firstItemIndex,
|
||||||
async (firstItemIndex: number) => queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
|
async (firstItemIndex: number) =>
|
||||||
|
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex),
|
||||||
);
|
);
|
||||||
|
|
||||||
useQueryCopilot.getState().setQueryResults(queryResults);
|
useQueryCopilot.getState().setQueryResults(queryResults);
|
||||||
@@ -185,7 +186,7 @@ export const QueryDocumentsPerPage = async (
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isCopilotActive = StringUtility.toBoolean(
|
const isCopilotActive = StringUtility.toBoolean(
|
||||||
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`)
|
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`),
|
||||||
);
|
);
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
|
|||||||
@@ -17,6 +17,37 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<QueryCopilotPromptbar
|
||||||
|
explorer={
|
||||||
|
Explorer {
|
||||||
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
|
"isTabsContentExpanded": [Function],
|
||||||
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {
|
||||||
|
"armResourceId": undefined,
|
||||||
|
"retryOptions": Object {
|
||||||
|
"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"
|
||||||
>
|
>
|
||||||
@@ -25,10 +56,10 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
onDragEnd={null}
|
onDragEnd={null}
|
||||||
onDragStart={null}
|
onDragStart={null}
|
||||||
onSecondaryPaneSizeChange={null}
|
onSecondaryPaneSizeChange={null}
|
||||||
percentage={false}
|
percentage={true}
|
||||||
primaryIndex={0}
|
primaryIndex={0}
|
||||||
primaryMinSize={100}
|
primaryMinSize={30}
|
||||||
secondaryMinSize={200}
|
secondaryMinSize={70}
|
||||||
vertical={true}
|
vertical={true}
|
||||||
>
|
>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
<button
|
<button
|
||||||
class="filterbtnstyle queryButton"
|
class="filterbtnstyle queryButton"
|
||||||
data-bind="
|
data-bind="
|
||||||
click: refreshDocumentsGrid,
|
click: refreshDocumentsGrid.bind($data, true),
|
||||||
enable: applyFilterButton.enabled"
|
enable: applyFilterButton.enabled"
|
||||||
aria-label="Apply filter"
|
aria-label="Apply filter"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
<img
|
<img
|
||||||
class="refreshcol"
|
class="refreshcol"
|
||||||
src="/refresh-cosmos.svg"
|
src="/refresh-cosmos.svg"
|
||||||
data-bind="click: refreshDocumentsGrid"
|
data-bind="click: refreshDocumentsGrid.bind($data, false)"
|
||||||
alt="Refresh documents"
|
alt="Refresh documents"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
/>
|
/>
|
||||||
@@ -209,7 +209,10 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="loadMore">
|
<div class="loadMore">
|
||||||
<a role="button" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0"
|
<a
|
||||||
|
role="button"
|
||||||
|
data-bind="click: loadNextPage.bind($data, false), event: { keypress: onLoadMoreKeyInput }"
|
||||||
|
tabindex="0"
|
||||||
>Load more</a
|
>Load more</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
|
import { format } from "react-string-format";
|
||||||
|
import { QueryConstants } from "Shared/Constants";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
|
||||||
import NewDocumentIcon from "../../../images/NewDocument.svg";
|
import NewDocumentIcon from "../../../images/NewDocument.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
|
||||||
import UploadIcon from "../../../images/Upload_16x16.svg";
|
import UploadIcon from "../../../images/Upload_16x16.svg";
|
||||||
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import {
|
import {
|
||||||
DocumentsGridMetrics,
|
DocumentsGridMetrics,
|
||||||
@@ -14,15 +17,15 @@ import {
|
|||||||
QueryCopilotSampleContainerId,
|
QueryCopilotSampleContainerId,
|
||||||
QueryCopilotSampleDatabaseId,
|
QueryCopilotSampleDatabaseId,
|
||||||
} from "../../Common/Constants";
|
} from "../../Common/Constants";
|
||||||
|
import editable from "../../Common/EditableUtility";
|
||||||
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||||
|
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||||
import { readDocument } from "../../Common/dataAccess/readDocument";
|
import { readDocument } from "../../Common/dataAccess/readDocument";
|
||||||
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||||
import editable from "../../Common/EditableUtility";
|
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
|
||||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
|
||||||
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
|
||||||
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 { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
@@ -30,6 +33,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
import * as QueryUtils from "../../Utils/QueryUtils";
|
import * as QueryUtils from "../../Utils/QueryUtils";
|
||||||
|
import { extractPartitionKeyValues } from "../../Utils/QueryUtils";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { useDialog } from "../Controls/Dialog";
|
import { useDialog } from "../Controls/Dialog";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
@@ -79,6 +83,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
private _resourceTokenPartitionKey: string;
|
private _resourceTokenPartitionKey: string;
|
||||||
private _isQueryCopilotSampleContainer: boolean;
|
private _isQueryCopilotSampleContainer: boolean;
|
||||||
private queryAbortController: AbortController;
|
private queryAbortController: AbortController;
|
||||||
|
private cancelQueryTimeoutID: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -346,6 +351,22 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query first page of documents
|
||||||
|
* Select and query first document and display content
|
||||||
|
*/
|
||||||
|
private async autoPopulateContent(applyFilterButtonPressed?: boolean) {
|
||||||
|
// reset iterator
|
||||||
|
this._documentsIterator = this.createIterator();
|
||||||
|
// load documents
|
||||||
|
await this.loadNextPage(applyFilterButtonPressed);
|
||||||
|
|
||||||
|
// Select first document and load content
|
||||||
|
if (this.documentIds().length > 0) {
|
||||||
|
this.documentIds()[0].click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onShowFilterClick(): Q.Promise<any> {
|
public onShowFilterClick(): Q.Promise<any> {
|
||||||
this.isFilterCreated(true);
|
this.isFilterCreated(true);
|
||||||
this.isFilterExpanded(true);
|
this.isFilterExpanded(true);
|
||||||
@@ -375,15 +396,14 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
public async refreshDocumentsGrid(): Promise<void> {
|
public async refreshDocumentsGrid(applyFilterButtonPressed?: boolean): Promise<void> {
|
||||||
// clear documents grid
|
// clear documents grid
|
||||||
this.documentIds([]);
|
this.documentIds([]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// reset iterator
|
// reset iterator
|
||||||
this._documentsIterator = this.createIterator();
|
this._documentsIterator = this.createIterator();
|
||||||
// load documents
|
// load documents
|
||||||
await this.loadNextPage();
|
await this.autoPopulateContent(applyFilterButtonPressed);
|
||||||
// collapse filter
|
// collapse filter
|
||||||
this.appliedFilter(this.filterContent());
|
this.appliedFilter(this.filterContent());
|
||||||
this.isFilterExpanded(false);
|
this.isFilterExpanded(false);
|
||||||
@@ -406,6 +426,11 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
this.queryAbortController.abort();
|
this.queryAbortController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO Doesn't seem to be used: remove?
|
||||||
|
* @param clickedDocumentId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> {
|
public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> {
|
||||||
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
|
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
|
||||||
return Q();
|
return Q();
|
||||||
@@ -455,7 +480,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
|
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
|
||||||
this.selectedDocumentContent.setBaseline(value);
|
this.selectedDocumentContent.setBaseline(value);
|
||||||
this.initialDocumentContent(value);
|
this.initialDocumentContent(value);
|
||||||
const partitionKeyValueArray = extractPartitionKey(
|
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues(
|
||||||
savedDocument,
|
savedDocument,
|
||||||
this.partitionKey as PartitionKeyDefinition,
|
this.partitionKey as PartitionKeyDefinition,
|
||||||
);
|
);
|
||||||
@@ -506,7 +531,10 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
const selectedDocumentId = this.selectedDocumentId();
|
const selectedDocumentId = this.selectedDocumentId();
|
||||||
const documentContent = JSON.parse(this.selectedDocumentContent());
|
const documentContent = JSON.parse(this.selectedDocumentContent());
|
||||||
|
|
||||||
const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition);
|
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues(
|
||||||
|
documentContent,
|
||||||
|
this.partitionKey as PartitionKeyDefinition,
|
||||||
|
);
|
||||||
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
|
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
|
||||||
|
|
||||||
this.isExecutionError(false);
|
this.isExecutionError(false);
|
||||||
@@ -624,8 +652,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
|
|
||||||
if (!this._documentsIterator) {
|
if (!this._documentsIterator) {
|
||||||
try {
|
try {
|
||||||
this._documentsIterator = this.createIterator();
|
await this.autoPopulateContent();
|
||||||
await this.loadNextPage();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
@@ -716,9 +743,35 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
this.initDocumentEditor(documentId, content);
|
this.initDocumentEditor(documentId, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadNextPage(): Q.Promise<any> {
|
public loadNextPage(applyFilterButtonClicked?: boolean): Q.Promise<any> {
|
||||||
this.isExecuting(true);
|
this.isExecuting(true);
|
||||||
this.isExecutionError(false);
|
this.isExecutionError(false);
|
||||||
|
let automaticallyCancelQueryAfterTimeout: boolean;
|
||||||
|
if (applyFilterButtonClicked && this.queryTimeoutEnabled()) {
|
||||||
|
const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout);
|
||||||
|
automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean(
|
||||||
|
StorageKey.AutomaticallyCancelQueryAfterTimeout,
|
||||||
|
);
|
||||||
|
const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => {
|
||||||
|
if (this.isExecuting()) {
|
||||||
|
if (automaticallyCancelQueryAfterTimeout) {
|
||||||
|
this.queryAbortController.abort();
|
||||||
|
} else {
|
||||||
|
useDialog
|
||||||
|
.getState()
|
||||||
|
.showOkCancelModalDialog(
|
||||||
|
QueryConstants.CancelQueryTitle,
|
||||||
|
format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached),
|
||||||
|
"Yes",
|
||||||
|
() => this.queryAbortController.abort(),
|
||||||
|
"No",
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, queryTimeout);
|
||||||
|
this.cancelQueryTimeoutID = cancelQueryTimeoutID;
|
||||||
|
}
|
||||||
return this._loadNextPageInternal()
|
return this._loadNextPageInternal()
|
||||||
.then(
|
.then(
|
||||||
(documentsIdsResponse = []) => {
|
(documentsIdsResponse = []) => {
|
||||||
@@ -774,7 +827,15 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(() => this.isExecuting(false));
|
.finally(() => {
|
||||||
|
this.isExecuting(false);
|
||||||
|
if (applyFilterButtonClicked && this.queryTimeoutEnabled()) {
|
||||||
|
clearTimeout(this.cancelQueryTimeoutID);
|
||||||
|
if (!automaticallyCancelQueryAfterTimeout) {
|
||||||
|
useDialog.getState().closeDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
|
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
|
||||||
@@ -952,4 +1013,8 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private queryTimeoutEnabled(): boolean {
|
||||||
|
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
||||||
|
import { extractPartitionKeyValues } from "Utils/QueryUtils";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -88,7 +89,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
|||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
(savedDocument: any) => {
|
(savedDocument: any) => {
|
||||||
let partitionKeyArray = extractPartitionKey(
|
const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues(
|
||||||
savedDocument,
|
savedDocument,
|
||||||
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
||||||
);
|
);
|
||||||
@@ -150,7 +151,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
|||||||
|
|
||||||
this.documentIds().forEach((documentId: DocumentId) => {
|
this.documentIds().forEach((documentId: DocumentId) => {
|
||||||
if (documentId.rid === updatedDocument._rid) {
|
if (documentId.rid === updatedDocument._rid) {
|
||||||
const partitionKeyArray = extractPartitionKey(
|
const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues(
|
||||||
updatedDocument,
|
updatedDocument,
|
||||||
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
||||||
);
|
);
|
||||||
@@ -289,7 +290,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _hasShardKeySpecified(document: any): boolean {
|
private _hasShardKeySpecified(document: any): boolean {
|
||||||
return Boolean(extractPartitionKey(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition));
|
return Boolean(extractPartitionKeyValues(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getPartitionKeyDefinition(): DataModels.PartitionKey {
|
private _getPartitionKeyDefinition(): DataModels.PartitionKey {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||||
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";
|
||||||
|
import { QueryConstants } from "Shared/Constants";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import SplitterLayout from "react-splitter-layout";
|
import SplitterLayout from "react-splitter-layout";
|
||||||
import "react-splitter-layout/lib/index.css";
|
import "react-splitter-layout/lib/index.css";
|
||||||
|
import { format } from "react-string-format";
|
||||||
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||||
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
@@ -80,6 +84,7 @@ interface IQueryTabStates {
|
|||||||
isExecuting: boolean;
|
isExecuting: boolean;
|
||||||
showCopilotSidebar: boolean;
|
showCopilotSidebar: boolean;
|
||||||
queryCopilotGeneratedQuery: string;
|
queryCopilotGeneratedQuery: string;
|
||||||
|
cancelQueryTimeoutID: NodeJS.Timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
||||||
@@ -107,13 +112,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
|
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
|
||||||
queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
|
queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
|
||||||
|
cancelQueryTimeoutID: undefined,
|
||||||
};
|
};
|
||||||
this.isCloseClicked = false;
|
this.isCloseClicked = false;
|
||||||
this.splitterId = this.props.tabId + "_splitter";
|
this.splitterId = this.props.tabId + "_splitter";
|
||||||
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
||||||
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
||||||
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
|
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
|
||||||
|
|
||||||
this.executeQueryButton = {
|
this.executeQueryButton = {
|
||||||
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -250,6 +255,34 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
this.setState({
|
this.setState({
|
||||||
isExecuting: true,
|
isExecuting: true,
|
||||||
});
|
});
|
||||||
|
let automaticallyCancelQueryAfterTimeout: boolean;
|
||||||
|
if (this.queryTimeoutEnabled()) {
|
||||||
|
const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout);
|
||||||
|
automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean(
|
||||||
|
StorageKey.AutomaticallyCancelQueryAfterTimeout,
|
||||||
|
);
|
||||||
|
const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => {
|
||||||
|
if (this.state.isExecuting) {
|
||||||
|
if (automaticallyCancelQueryAfterTimeout) {
|
||||||
|
this.queryAbortController.abort();
|
||||||
|
} else {
|
||||||
|
useDialog
|
||||||
|
.getState()
|
||||||
|
.showOkCancelModalDialog(
|
||||||
|
QueryConstants.CancelQueryTitle,
|
||||||
|
format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached),
|
||||||
|
"Yes",
|
||||||
|
() => this.queryAbortController.abort(),
|
||||||
|
"No",
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, queryTimeout);
|
||||||
|
this.setState({
|
||||||
|
cancelQueryTimeoutID,
|
||||||
|
});
|
||||||
|
}
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -273,7 +306,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
this.props.tabsBaseInstance.isExecuting(false);
|
this.props.tabsBaseInstance.isExecuting(false);
|
||||||
this.setState({
|
this.setState({
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
|
cancelQueryTimeoutID: undefined,
|
||||||
});
|
});
|
||||||
|
if (this.queryTimeoutEnabled()) {
|
||||||
|
clearTimeout(this.state.cancelQueryTimeoutID);
|
||||||
|
if (!automaticallyCancelQueryAfterTimeout) {
|
||||||
|
useDialog.getState().closeDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
this.togglesOnFocus();
|
this.togglesOnFocus();
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
@@ -405,6 +445,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
return this.state.sqlQueryEditorContent;
|
return this.state.sqlQueryEditorContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private queryTimeoutEnabled(): boolean {
|
||||||
|
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
private unsubscribeCopilotSidebar: () => void;
|
private unsubscribeCopilotSidebar: () => void;
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { extractPartitionKey } from "@azure/cosmos";
|
import { extractPartitionKeyValues } from "Utils/QueryUtils";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { readDocument } from "../../Common/dataAccess/readDocument";
|
import { readDocument } from "../../Common/dataAccess/readDocument";
|
||||||
@@ -42,7 +42,7 @@ export default class ConflictId {
|
|||||||
}
|
}
|
||||||
this.partitionKeyProperty = container && container.partitionKeyProperty;
|
this.partitionKeyProperty = container && container.partitionKeyProperty;
|
||||||
this.partitionKey = container && container.partitionKey;
|
this.partitionKey = container && container.partitionKey;
|
||||||
this.partitionKeyValue = extractPartitionKey(this.parsedContent, this.partitionKey as any);
|
this.partitionKeyValue = extractPartitionKeyValues(this.parsedContent, this.partitionKey as any);
|
||||||
this.stringPartitionKeyValue = this.getPartitionKeyValueAsString();
|
this.stringPartitionKeyValue = this.getPartitionKeyValueAsString();
|
||||||
this.id = ko.observable(data.id);
|
this.id = ko.observable(data.id);
|
||||||
this.isDirty = ko.observable(false);
|
this.isDirty = ko.observable(false);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
jest.mock("../../../hooks/useDirectories");
|
jest.mock("../../../hooks/useDirectories");
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { extractFeatures } from "Platform/Hosted/extractFeatures";
|
||||||
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ConnectExplorer } from "./ConnectExplorer";
|
import { ConnectExplorer } from "./ConnectExplorer";
|
||||||
|
|
||||||
@@ -16,3 +18,24 @@ it("shows the connect form", () => {
|
|||||||
fireEvent.click(screen.getByText("Connect to your account with connection string"));
|
fireEvent.click(screen.getByText("Connect to your account with connection string"));
|
||||||
expect(screen.queryByPlaceholderText("Please enter a connection string")).toBeDefined();
|
expect(screen.queryByPlaceholderText("Please enter a connection string")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("hides the connection string link when feature.disableConnectionStringLogin is true", () => {
|
||||||
|
const connectionString = "fakeConnectionString";
|
||||||
|
const login = jest.fn();
|
||||||
|
const setConnectionString = jest.fn();
|
||||||
|
const setEncryptedToken = jest.fn();
|
||||||
|
const setAuthType = jest.fn();
|
||||||
|
const oldFeatures = userContext.features;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
"feature.disableConnectionStringLogin": "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
const testFeatures = extractFeatures(params);
|
||||||
|
updateUserContext({ features: testFeatures });
|
||||||
|
|
||||||
|
render(<ConnectExplorer {...{ login, setEncryptedToken, setAuthType, connectionString, setConnectionString }} />);
|
||||||
|
expect(screen.queryByPlaceholderText("Connect to your account with connection string")).toBeNull();
|
||||||
|
|
||||||
|
updateUserContext({ features: oldFeatures });
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||||
import ErrorImage from "../../../../images/error.svg";
|
import ErrorImage from "../../../../images/error.svg";
|
||||||
@@ -37,6 +38,7 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
setConnectionString,
|
setConnectionString,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
|
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
|
||||||
|
const enableConnectionStringLogin = !userContext.features.disableConnectionStringLogin;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
|
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
|
||||||
@@ -46,7 +48,7 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
<img src={ConnectImage} alt="Azure Cosmos DB" />
|
<img src={ConnectImage} alt="Azure Cosmos DB" />
|
||||||
</p>
|
</p>
|
||||||
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
|
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
|
||||||
{isFormVisible ? (
|
{isFormVisible && enableConnectionStringLogin ? (
|
||||||
<form
|
<form
|
||||||
id="connectWithConnectionString"
|
id="connectWithConnectionString"
|
||||||
onSubmit={async (event) => {
|
onSubmit={async (event) => {
|
||||||
@@ -89,9 +91,11 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div id="connectWithAad">
|
<div id="connectWithAad">
|
||||||
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
|
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
|
||||||
<p className="switchConnectTypeText" onClick={showForm}>
|
{enableConnectionStringLogin && (
|
||||||
Connect to your account with connection string
|
<p className="switchConnectTypeText" onClick={showForm}>
|
||||||
</p>
|
Connect to your account with connection string
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export type Features = {
|
|||||||
readonly enableCopilotFullSchema: boolean;
|
readonly enableCopilotFullSchema: boolean;
|
||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
|
readonly disableConnectionStringLogin: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -114,6 +115,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
|
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
|
||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,3 +208,9 @@ export class FreeTierLimits {
|
|||||||
public static RU: number = 1000;
|
public static RU: number = 1000;
|
||||||
public static Storage: number = 25;
|
public static Storage: number = 25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class QueryConstants {
|
||||||
|
public static readonly CancelQueryTitle: string = "Cancel query";
|
||||||
|
public static readonly CancelQuerySubTextTemplate: string = "{0} Do you want to cancel this query?";
|
||||||
|
public static readonly CancelQueryTimeoutThresholdReached: string = "The query timeout threshold has been reached.";
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PriorityLevel } from "@azure/cosmos";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { LocalStorageUtility, StorageKey } from "./StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "./StorageUtility";
|
||||||
|
|
||||||
@@ -6,7 +7,7 @@ export const createDefaultSettings = () => {
|
|||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage);
|
||||||
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true");
|
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true");
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, Constants.Queries.DefaultMaxDegreeOfParallelism);
|
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, Constants.Queries.DefaultMaxDegreeOfParallelism);
|
||||||
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, Constants.PriorityLevel.Default);
|
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, PriorityLevel.Low);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasSettingsDefined = (): boolean => {
|
export const hasSettingsDefined = (): boolean => {
|
||||||
@@ -19,6 +20,6 @@ export const hasSettingsDefined = (): boolean => {
|
|||||||
|
|
||||||
export const ensurePriorityLevel = () => {
|
export const ensurePriorityLevel = () => {
|
||||||
if (!LocalStorageUtility.hasItem(StorageKey.PriorityLevel)) {
|
if (!LocalStorageUtility.hasItem(StorageKey.PriorityLevel)) {
|
||||||
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, Constants.PriorityLevel.Default);
|
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, PriorityLevel.Low);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import * as SessionStorageUtility from "./SessionStorageUtility";
|
|||||||
export { LocalStorageUtility, SessionStorageUtility };
|
export { LocalStorageUtility, SessionStorageUtility };
|
||||||
export enum StorageKey {
|
export enum StorageKey {
|
||||||
ActualItemPerPage,
|
ActualItemPerPage,
|
||||||
|
QueryTimeoutEnabled,
|
||||||
|
QueryTimeout,
|
||||||
|
AutomaticallyCancelQueryAfterTimeout,
|
||||||
ContainerPaginationEnabled,
|
ContainerPaginationEnabled,
|
||||||
CustomItemPerPage,
|
CustomItemPerPage,
|
||||||
DatabaseAccountId,
|
DatabaseAccountId,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as PriorityBasedExecutionUtils from "./PriorityBasedExecutionUtils";
|
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
|
||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
|
import { PriorityLevel } from "@azure/cosmos";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import * as PriorityBasedExecutionUtils from "./PriorityBasedExecutionUtils";
|
||||||
|
|
||||||
describe("Priority execution utility", () => {
|
describe("Priority execution utility", () => {
|
||||||
it("check default priority level is Low", () => {
|
it("check default priority level is Low", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
|
import { Constants, PriorityLevel } from "@azure/cosmos";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
export function isFeatureEnabled(): boolean {
|
export function isFeatureEnabled(): boolean {
|
||||||
@@ -21,18 +21,18 @@ export function isRelevantRequest(requestContext: Cosmos.RequestContext): boolea
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPriorityLevel(): PriorityLevel {
|
export function getPriorityLevel(): PriorityLevel {
|
||||||
const priorityLevel = LocalStorageUtility.getEntryString(StorageKey.PriorityLevel);
|
const priorityLevel: string = LocalStorageUtility.getEntryString(StorageKey.PriorityLevel);
|
||||||
if (priorityLevel && Object.values(PriorityLevel).includes(priorityLevel)) {
|
if (priorityLevel) {
|
||||||
return priorityLevel as PriorityLevel;
|
return priorityLevel as PriorityLevel;
|
||||||
} else {
|
} else {
|
||||||
return PriorityLevel.Default;
|
return PriorityLevel.Low;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
|
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, undefined, next) => {
|
||||||
if (isRelevantRequest(requestContext)) {
|
if (isRelevantRequest(requestContext)) {
|
||||||
const priorityLevel: PriorityLevel = getPriorityLevel();
|
const priorityLevel: PriorityLevel = getPriorityLevel();
|
||||||
requestContext.headers["x-ms-cosmos-priority-level"] = priorityLevel as string;
|
requestContext.headers[Constants.HttpHeaders.PriorityLevel] = priorityLevel;
|
||||||
}
|
}
|
||||||
return next(requestContext);
|
return next(requestContext);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { PartitionKey, PartitionKeyDefinition, PartitionKeyKind } from "@azure/cosmos";
|
||||||
import * as Q from "q";
|
import * as Q from "q";
|
||||||
import * as sinon from "sinon";
|
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";
|
||||||
|
|
||||||
describe("Query Utils", () => {
|
describe("Query Utils", () => {
|
||||||
const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => {
|
const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => {
|
||||||
@@ -94,4 +96,69 @@ describe("Query Utils", () => {
|
|||||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("extractPartitionKey", () => {
|
||||||
|
const documentContent = {
|
||||||
|
"Volcano Name": "Adams",
|
||||||
|
Country: "United States",
|
||||||
|
Region: "US-Washington",
|
||||||
|
Location: {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: [-121.49, 46.206],
|
||||||
|
},
|
||||||
|
Elevation: 3742,
|
||||||
|
Type: "Stratovolcano",
|
||||||
|
Status: "Tephrochronology",
|
||||||
|
"Last Known Eruption": "Last known eruption from A.D. 1-1499, inclusive",
|
||||||
|
id: "9e3c494e-8367-3f50-1f56-8c6fcb961363",
|
||||||
|
_rid: "xzo0AJRYUxUFAAAAAAAAAA==",
|
||||||
|
_self: "dbs/xzo0AA==/colls/xzo0AJRYUxU=/docs/xzo0AJRYUxUFAAAAAAAAAA==/",
|
||||||
|
_etag: '"ce00fa43-0000-0100-0000-652840440000"',
|
||||||
|
_attachments: "attachments/",
|
||||||
|
_ts: 1697136708,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should extract single partition key value", () => {
|
||||||
|
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
|
||||||
|
kind: PartitionKeyKind.Hash,
|
||||||
|
paths: ["/Elevation"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
||||||
|
documentContent,
|
||||||
|
singlePartitionKeyDefinition,
|
||||||
|
);
|
||||||
|
expect(partitionKeyValues.length).toBe(1);
|
||||||
|
expect(partitionKeyValues[0]).toEqual(3742);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract two partition key values", () => {
|
||||||
|
const multiPartitionKeyDefinition: PartitionKeyDefinition = {
|
||||||
|
kind: PartitionKeyKind.MultiHash,
|
||||||
|
paths: ["/Type", "/Status"],
|
||||||
|
};
|
||||||
|
const expectedPartitionKeyValues: string[] = ["Stratovolcano", "Tephrochronology"];
|
||||||
|
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
||||||
|
documentContent,
|
||||||
|
multiPartitionKeyDefinition,
|
||||||
|
);
|
||||||
|
expect(partitionKeyValues.length).toBe(2);
|
||||||
|
expect(expectedPartitionKeyValues).toContain(documentContent["Type"]);
|
||||||
|
expect(expectedPartitionKeyValues).toContain(documentContent["Status"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract no partition key values", () => {
|
||||||
|
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
|
||||||
|
kind: PartitionKeyKind.Hash,
|
||||||
|
paths: ["/InvalidPartitionKeyPath"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
||||||
|
documentContent,
|
||||||
|
singlePartitionKeyDefinition,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(partitionKeyValues.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
|
||||||
@@ -82,3 +83,22 @@ export const queryPagesUntilContentPresent = async (
|
|||||||
|
|
||||||
return await doRequest(firstItemIndex);
|
return await doRequest(firstItemIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
export const extractPartitionKeyValues = (
|
||||||
|
documentContent: any,
|
||||||
|
partitionKeyDefinition: PartitionKeyDefinition,
|
||||||
|
): PartitionKey[] => {
|
||||||
|
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const partitionKeyValues: PartitionKey[] = [];
|
||||||
|
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
|
||||||
|
const partitionKeyPathWithoutSlash: string = partitionKeyPath.substring(1);
|
||||||
|
if (documentContent[partitionKeyPathWithoutSlash]) {
|
||||||
|
partitionKeyValues.push(documentContent[partitionKeyPathWithoutSlash]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return partitionKeyValues;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ import { createUri } from "Common/UrlUtility";
|
|||||||
import { FabricMessage } from "Contracts/FabricContract";
|
import { FabricMessage } from "Contracts/FabricContract";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { fetchEncryptedToken } from "Platform/Hosted/Components/ConnectExplorer";
|
|
||||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||||
import { fetchAccessData } from "hooks/usePortalAccessToken";
|
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { AccountKind, Flights } from "../Common/Constants";
|
import { AccountKind, Flights } from "../Common/Constants";
|
||||||
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
||||||
import { sendMessage, sendReadyMessage } from "../Common/MessageHandler";
|
import { handleCachedDataMessage, sendMessage, sendReadyMessage } from "../Common/MessageHandler";
|
||||||
import { Platform, configContext, updateConfigContext } from "../ConfigContext";
|
import { Platform, configContext, updateConfigContext } from "../ConfigContext";
|
||||||
import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
|
import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
@@ -107,23 +105,7 @@ async function configureFabric(): Promise<Explorer> {
|
|||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case "initialize": {
|
case "initialize": {
|
||||||
// TODO For now, retrieve info from session storage. Replace with info injected into Data Explorer
|
explorer = await configureWithFabric(data.message.endpoint);
|
||||||
const connectionString = data.connectionString ?? sessionStorage.getItem("connectionString");
|
|
||||||
if (!connectionString) {
|
|
||||||
console.error("No connection string found in session storage");
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const encryptedToken = await fetchEncryptedToken(connectionString);
|
|
||||||
// TODO Duplicated from useTokenMetadata
|
|
||||||
const encryptedTokenMetadata = await fetchAccessData(encryptedToken);
|
|
||||||
|
|
||||||
const hostedConfig: EncryptedToken = {
|
|
||||||
authType: AuthType.EncryptedToken,
|
|
||||||
encryptedToken,
|
|
||||||
encryptedTokenMetadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
explorer = await configureWithEncryptedToken(hostedConfig);
|
|
||||||
resolve(explorer);
|
resolve(explorer);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -166,6 +148,10 @@ async function configureFabric(): Promise<Explorer> {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "authorizationToken": {
|
||||||
|
handleCachedDataMessage(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown Fabric message type: ${JSON.stringify(data)}`);
|
console.error(`Unknown Fabric message type: ${JSON.stringify(data)}`);
|
||||||
break;
|
break;
|
||||||
@@ -315,6 +301,25 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
|
|||||||
return explorer;
|
return explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function configureWithFabric(documentEndpoint: string): Explorer {
|
||||||
|
updateUserContext({
|
||||||
|
authType: AuthType.ConnectionString,
|
||||||
|
databaseAccount: {
|
||||||
|
id: "",
|
||||||
|
location: "",
|
||||||
|
type: "",
|
||||||
|
name: "Mounted",
|
||||||
|
kind: AccountKind.Default,
|
||||||
|
properties: {
|
||||||
|
documentEndpoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const explorer = new Explorer();
|
||||||
|
setTimeout(() => explorer.refreshAllDatabases(), 0);
|
||||||
|
return explorer;
|
||||||
|
}
|
||||||
|
|
||||||
function configureWithEncryptedToken(config: EncryptedToken): Explorer {
|
function configureWithEncryptedToken(config: EncryptedToken): Explorer {
|
||||||
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
|
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
|||||||
|
|
||||||
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
|
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
|
||||||
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
|
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
|
||||||
closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }),
|
closeFeedbackModal: () => set({ showFeedbackModal: false }),
|
||||||
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
|
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
|
||||||
set({ hideFeedbackModalForLikedQueries }),
|
set({ hideFeedbackModalForLikedQueries }),
|
||||||
refreshCorrelationId: () => set({ correlationId: guid() }),
|
refreshCorrelationId: () => set({ correlationId: guid() }),
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
<mimeMap fileExtension="woff" mimeType="application/font-woff" />
|
<mimeMap fileExtension="woff" mimeType="application/font-woff" />
|
||||||
</staticContent>
|
</staticContent>
|
||||||
<rewrite>
|
<rewrite>
|
||||||
|
<rules>
|
||||||
|
<rule name="AAD-Redirect" stopProcessing="true">
|
||||||
|
<match url="^aad" ignoreCase="true"/>
|
||||||
|
<conditions>
|
||||||
|
<add input="{HTTP_HOST}" pattern="^cosmos.azure.com" />
|
||||||
|
</conditions>
|
||||||
|
<action type="Redirect" url="/?feature.enableAadDataPlane=true&feature.disableConnectionStringLogin=true" redirectType="Permanent" />
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
<outboundRules>
|
<outboundRules>
|
||||||
<rule name="Strict-Transport-Security" enabled="true">
|
<rule name="Strict-Transport-Security" enabled="true">
|
||||||
<match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
|
<match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
|
||||||
|
|||||||
Reference in New Issue
Block a user