Compare commits

..

19 Commits

Author SHA1 Message Date
Asier Isayas
a96f4bbb46 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/pbe 2023-10-19 15:28:04 -04:00
Asier Isayas
8075ef2847 Upgrade Cosmos SDK to 4.0.0 (#1664)
* upgrade cosmos sdk to 4.0.0

* added explicit any test

* fixed package-lock.json

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2023-10-19 15:22:12 -04:00
Asier Isayas
063ad23bce Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/pbe 2023-10-19 13:49:55 -04:00
Asier Isayas
eacbeae417 set default priority level to Low 2023-10-19 13:45:47 -04:00
Asier Isayas
94158504a8 Cancel query timeout (#1651)
* cancel query option

* query timeout

* run prettier

* removed comments

* fixed npm run compile errors

* fixed tests

* fixed unit test  errors

* fixed unit test  errors

* fixed unit test  errors

* fixed unit test  errors

* fixed unit test  errors

* increased min timeout

* added automatican cancel query option

* added react string format

* npm run format

* added unless automatic cancellation has been enabled

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2023-10-19 13:21:39 -04:00
Laurent Nguyen
9b032ecae4 Turn off "New Database" for Fabric (#1663)
* Turn off "New Database" for Fabric

* Fix format
2023-10-18 08:30:45 +00:00
Asier Isayas
6493c985b4 fixed package-lock.json 2023-10-17 14:23:51 -04:00
Asier Isayas
569167fa10 fixed merge conflicts 2023-10-17 13:52:28 -04:00
Asier Isayas
6e267b2bba added explicit any test 2023-10-17 12:53:52 -04:00
jawelton74
14d7677056 Add feature for disabling connection string login and enable this for aad redirect. (#1660)
* Add redirect for /aad to /?feature.enableAadDataPlane=true

* Add feature to hide the connection string login link. Enable this new
feature for the aad redirect.
2023-10-16 13:18:40 -07:00
Asier Isayas
282004b09b upgrade cosmos sdk to 4.0.0 2023-10-13 12:15:36 -04:00
sunghyunkang1111
d376a7463c P1, P2 bug fixes for private preview (#1657)
* P1 bug fix for private preview

* Add updated snapshot files

* Fix failing unit test

* Fix unit tests and update snapshot
2023-10-12 21:54:01 -05:00
Laurent Nguyen
9669301d14 Remove obsolete unit test (#1655) 2023-10-12 08:00:08 +02:00
Laurent Nguyen
dcd8d1637b Implement retrieval of authorization token for Fabric via iframe rpc (#1647)
* For Fabric, send message to get Authorization token from iframe parent

* tokenProvider: set date header and return token

* Expect account endpoint on initialize message from Fabric

* Fix format

---------

Co-authored-by: artrejo <artrejo@microsoft.com>
2023-10-10 12:25:58 -07:00
Laurent Nguyen
f36fccd3ef Auto-select first item in DocumentsTab. Fix selection highlighting (#1645)
* Auto-select first document in documentsTab

* Fix style row selection by making selector more specific
2023-10-10 07:19:35 +02:00
jawelton74
8ff9a84004 Add redirect for /aad to /?feature.enableAadDataPlane=true (#1649) 2023-10-09 11:31:36 -07:00
sunghyunkang1111
e42e24b175 Redesign copilot (#1650)
* Add copilot toggle button

* take out toggle button when closing tab

* Update snapshots

* Fix the prettier issue

* fix prettier
2023-10-09 12:42:25 -05:00
Laurent Nguyen
44d6f29edd Revert "For Fabric, send message to get Authorization token from iframe parent"
This reverts commit 9db06af552.
2023-10-06 14:35:30 +00:00
Laurent Nguyen
9db06af552 For Fabric, send message to get Authorization token from iframe parent 2023-10-06 14:33:46 +00:00
41 changed files with 2024 additions and 1482 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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"
} }
} }

View File

@@ -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";

View File

@@ -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();
}); });
}); });

View File

@@ -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,

View File

@@ -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();
} }

View File

@@ -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 };

View File

@@ -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;
};

View 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;
}

View File

@@ -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({

View File

@@ -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(

View File

@@ -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);

View File

@@ -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">

View File

@@ -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 &quot;None&quot; 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"

View File

@@ -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"
> >

View File

@@ -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(),
}, },

View File

@@ -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>
); );
}; };

View File

@@ -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

View File

@@ -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}

View File

@@ -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, {

View File

@@ -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

View File

@@ -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>

View File

@@ -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);
}
} }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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 });
});

View File

@@ -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>

View File

@@ -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"),
}; };
} }

View File

@@ -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.";
}

View File

@@ -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);
} }
}; };

View File

@@ -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,

View File

@@ -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", () => {

View File

@@ -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);
}; };

View File

@@ -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);
});
});
}); });

View File

@@ -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;
};

View File

@@ -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({

View File

@@ -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() }),

View File

@@ -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&amp;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=".*" />