Perf/copilot cleanup and optimizations (#2447)

* perf: remove deprecated copilot feature, add ARM timeouts, fix race conditions

- Remove entire QueryCopilot feature (~50 files deleted, ~30 files cleaned)
- Remove CopilotConfigured and SampleDataLoaded metric phases
- Fix DatabaseTreeRendered 76% stuck rate (remove one-shot guard in useMetricPhases)
- Add 8s default timeout to ARM requests (AbortController-based)
- Fix MSAL token forceRefresh (true -> false, use cache)
- Add concurrency limit of 5 to collection loading in Explorer
- Remove orphaned SampleDataClient.ts and queryCopilotSampleData.json
- Clean up dead sampleDataConnectionInfo field from UserContext

* Clean up copilot and optimize initialization

* Clean up copilot and optimize initialization
This commit is contained in:
sunghyunkang1111
2026-04-08 11:25:33 -05:00
committed by GitHub
parent ab4f1289e1
commit fb250259ed
126 changed files with 162 additions and 22486 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -259,7 +259,6 @@ export class Areas {
public static Tab: string = "Tab";
public static ShareDialog: string = "Share Access Dialog";
public static Notebook: string = "Notebook";
public static Copilot: string = "Copilot";
public static CloudShell: string = "Cloud Shell";
}
@@ -456,7 +455,6 @@ export enum ContainerStatusType {
export enum PoolIdType {
DefaultPoolId = "default",
QueryCopilot = "query-copilot",
}
export const EmulatorMasterKey =
@@ -543,243 +541,6 @@ export class FeedbackLabels {
public static readonly provideFeedback: string = "Provide feedback";
}
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
export const QueryCopilotSampleContainerId = "SampleContainer";
export const QueryCopilotSampleContainerSchema = {
product: {
sampleData: {
id: "c415e70f-9bf5-4cda-aebe-a290cb8b94c2",
name: "Amazing Phone 3000 (Black)",
price: 223.33,
category: "Electronics",
description:
"This Amazing Phone 3000 (Black) is made of black metal! It has a very well made aluminum body and it feels very comfortable. We loved the sound that comes out of it! Also, the design of the phone was a little loose at first because I was using the camera and felt uncomfortable wearing it. The phone is actually made slightly smaller than these photos! This is due to the addition of a 3.3mm filter",
stock: 84,
countryOfOrigin: "USA",
firstAvailable: "2018-09-07 19:41:44",
priceHistory: [238.68, 234.7, 221.49, 205.88, 220.15],
customerRatings: [
{
username: "steven66",
firstName: "Carol",
gender: "female",
lastName: "Shelton",
age: "25-35",
area: "suburban",
address: "261 Collins Burgs Apt. 332\nNorth Taylor, NM 32268",
stars: 5,
date: "2021-04-22 13:42:14",
verifiedUser: true,
},
{
username: "khudson",
firstName: "Ronald",
gender: "male",
lastName: "Webb",
age: "18-24",
area: "suburban",
address: "9912 Parker Court Apt. 068\nNorth Austin, HI 76225",
stars: 5,
date: "2021-02-07 07:00:22",
verifiedUser: false,
},
{
username: "lfrancis",
firstName: "Brady",
gender: "male",
lastName: "Wright",
age: "35-45",
area: "urban",
address: "PSC 5437, Box 3159\nAPO AA 26385",
stars: 2,
date: "2022-02-23 21:40:10",
verifiedUser: false,
},
{
username: "nicolemartinez",
firstName: "Megan",
gender: "female",
lastName: "Tran",
age: "18-24",
area: "rural",
address: "7445 Salazar Brooks\nNew Sarah, PW 18097",
stars: 4,
date: "2021-09-01 22:21:40",
verifiedUser: false,
},
{
username: "uguzman",
firstName: "Deanna",
gender: "female",
lastName: "Campbell",
age: "18-24",
area: "urban",
address: "41104 Moreno Fort Suite 872\nPort Michaelbury, AK 48712",
stars: 1,
date: "2022-03-07 02:23:14",
verifiedUser: false,
},
{
username: "rebeccahunt",
firstName: "Jared",
gender: "male",
lastName: "Lopez",
age: "18-24",
area: "rural",
address: "392 Morgan Village Apt. 785\nGreenshire, CT 05921",
stars: 5,
date: "2021-04-17 04:17:49",
verifiedUser: false,
},
],
rareProperty: true,
},
schema: {
properties: {
id: {
type: "string",
},
name: {
type: "string",
},
price: {
type: "number",
},
category: {
type: "string",
},
description: {
type: "string",
},
stock: {
type: "number",
},
countryOfOrigin: {
type: "string",
},
firstAvailable: {
type: "string",
},
priceHistory: {
items: {
type: "number",
},
type: "array",
},
customerRatings: {
items: {
properties: {
username: {
type: "string",
},
firstName: {
type: "string",
},
gender: {
type: "string",
},
lastName: {
type: "string",
},
age: {
type: "string",
},
area: {
type: "string",
},
address: {
type: "string",
},
stars: {
type: "number",
},
date: {
type: "string",
},
verifiedUser: {
type: "boolean",
},
},
type: "object",
},
type: "array",
},
rareProperty: {
type: "boolean",
},
},
type: "object",
},
},
};
export const ShortenedQueryCopilotSampleContainerSchema = {
containerSchema: {
product: {
sampleData: {
categoryName: "Components, Saddles",
name: "LL Road Seat/Saddle",
price: 27.12,
tags: [
{
id: "0573D684-9140-4DEE-89AF-4E4A90E65666",
name: "Tag-113",
},
{
id: "6C2F05C8-1E61-4912-BE1A-C67A378429BB",
name: "Tag-5",
},
],
},
schema: {
properties: {
categoryName: {
type: "string",
},
name: {
type: "string",
},
price: {
type: "number",
},
tags: {
items: {
properties: {
id: {
type: "string",
},
name: {
type: "string",
},
},
type: "object",
},
type: "array",
},
},
type: "object",
},
},
},
userPrompt: "find all products",
};
export enum MongoGuidRepresentation {
Standard = "Standard",
CSharpLegacy = "CSharpLegacy",

View File

@@ -1,26 +0,0 @@
import * as Cosmos from "@azure/cosmos";
import { userContext } from "UserContext";
let _sampleDataclient: Cosmos.CosmosClient;
export function sampleDataClient(): Cosmos.CosmosClient {
if (_sampleDataclient) {
return _sampleDataclient;
}
const sampleDataConnectionInfo = userContext.sampleDataConnectionInfo;
const options: Cosmos.CosmosClientOptions = {
endpoint: sampleDataConnectionInfo.accountEndpoint,
tokenProvider: async () => {
const sampleDataConnectionInfo = userContext.sampleDataConnectionInfo;
return Promise.resolve(sampleDataConnectionInfo.resourceToken);
},
connectionPolicy: {
enableEndpointDiscovery: false,
},
userAgentSuffix: "Azure Portal",
};
_sampleDataclient = new Cosmos.CosmosClient(options);
return _sampleDataclient;
}

View File

@@ -1,6 +1,4 @@
import { CosmosClient } from "@azure/cosmos";
import { sampleDataClient } from "Common/SampleDataClient";
import { userContext } from "UserContext";
import * as DataModels from "../../Contracts/DataModels";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
@@ -10,20 +8,6 @@ export async function readCollection(databaseId: string, collectionId: string):
return await readCollectionInternal(cosmosClient, databaseId, collectionId);
}
export async function readSampleCollection(): Promise<DataModels.Collection> {
const cosmosClient = sampleDataClient();
const sampleDataConnectionInfo = userContext.sampleDataConnectionInfo;
const databaseId = sampleDataConnectionInfo?.databaseId;
const collectionId = sampleDataConnectionInfo?.collectionId;
if (!databaseId || !collectionId) {
return undefined;
}
return await readCollectionInternal(cosmosClient, databaseId, collectionId);
}
export async function readCollectionInternal(
cosmosClient: CosmosClient,
databaseId: string,

View File

@@ -735,10 +735,6 @@ export enum PhoenixErrorType {
UserMissingPermissionsError = "UserMissingPermissionsError",
}
export interface CopilotEnabledConfiguration {
isEnabled: boolean;
}
export interface FeatureRegistration {
name: string;
properties: {

View File

@@ -9,9 +9,6 @@ import {
import { useDatabases } from "Explorer/useDatabases";
import { Keys, t } from "Localization";
import { isFabric, isFabricNative, openRestoreContainerDialog } from "Platform/Fabric/FabricUtil";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import React from "react";
import AddCollectionIcon from "../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
@@ -218,29 +215,7 @@ export const createCollectionContextMenuButton = (
};
export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] => {
const items: TreeNodeMenuItem[] = [];
if (userContext.apiType === "SQL") {
const copilotVersion = userContext.features.copilotVersion;
if (copilotVersion === "v1.0") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
},
label: t(Keys.contextMenu.newSqlQuery),
});
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => sampleCollection && sampleCollection.onNewQueryClick(sampleCollection, undefined),
label: t(Keys.contextMenu.newSqlQuery),
});
}
}
return items;
return [];
};
export const createStoreProcedureContextMenuItems = (

View File

@@ -23,17 +23,10 @@ export class ContainerSampleGenerator {
/**
* Factory function to load the json data file
*/
public static async createSampleGeneratorAsync(
container: Explorer,
isCopilot?: boolean,
): Promise<ContainerSampleGenerator> {
public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> {
const generator = new ContainerSampleGenerator(container);
let dataFileContent: any;
if (isCopilot) {
dataFileContent = await import(
/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json"
);
} else if (userContext.apiType === "Gremlin") {
if (userContext.apiType === "Gremlin") {
dataFileContent = await import(
/* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json"
);

View File

@@ -1,12 +1,10 @@
import * as msal from "@azure/msal-browser";
import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { IGalleryItem } from "Juno/JunoClient";
import {
isFabricMirrored,
@@ -14,12 +12,10 @@ import {
isFabricNative,
scheduleRefreshFabricToken,
} from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout";
import React from "react";
import _ from "underscore";
@@ -27,11 +23,11 @@ import shallow from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient";
import { readCollection, readSampleCollection } from "../Common/dataAccess/readCollection";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import * as DataModels from "../Contracts/DataModels";
import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels";
@@ -471,15 +467,9 @@ export default class Explorer {
this._isInitializingNotebooks = false;
}
public async allocateContainer(poolId: PoolIdType, mode?: string): Promise<void> {
const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false;
const notebookServerInfo = shouldUseNotebookStates
? useNotebook.getState().notebookServerInfo
: useQueryCopilot.getState().notebookServerInfo;
const isAllocating = shouldUseNotebookStates
? useNotebook.getState().isAllocating
: useQueryCopilot.getState().isAllocatingContainer;
public async allocateContainer(): Promise<void> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const isAllocating = useNotebook.getState().isAllocating;
if (
isAllocating === false &&
(notebookServerInfo === undefined ||
@@ -489,34 +479,23 @@ export default class Explorer {
status: ConnectionStatusType.Connecting,
};
shouldUseNotebookStates && useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setConnectionInfo(connectionStatus);
let connectionInfo;
let provisionData: IProvisionData;
try {
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
if (shouldUseNotebookStates) {
useNotebook.getState().setIsAllocating(true);
provisionData = {
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
poolId: undefined,
};
} else {
useQueryCopilot.getState().setIsAllocatingContainer(true);
provisionData = {
poolId: poolId,
databaseId: useTabs.getState().activeTab.collection.databaseId,
containerId: useTabs.getState().activeTab.collection.id(),
mode: mode,
};
}
useNotebook.getState().setIsAllocating(true);
const provisionData: IProvisionData = {
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
poolId: undefined,
};
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`PhoenixServiceUrl is invalid!`);
}
await this.setNotebookInfo(shouldUseNotebookStates, connectionInfo, connectionStatus);
await this.setNotebookInfo(connectionInfo, connectionStatus);
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
@@ -527,27 +506,21 @@ export default class Explorer {
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
if (shouldUseNotebookStates) {
connectionStatus.status = ConnectionStatusType.Failed;
shouldUseNotebookStates
? useNotebook.getState().resetContainerConnection(connectionStatus)
: useQueryCopilot.getState().resetContainerConnection();
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
} else {
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
);
}
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetContainerConnection(connectionStatus);
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
} else {
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
);
}
throw error;
} finally {
shouldUseNotebookStates
? useNotebook.getState().setIsAllocating(false)
: useQueryCopilot.getState().setIsAllocatingContainer(false);
useNotebook.getState().setIsAllocating(false);
this.refreshCommandBarButtons();
this.refreshNotebookList();
this._isInitializingNotebooks = false;
@@ -556,7 +529,6 @@ export default class Explorer {
}
public async setNotebookInfo(
shouldUseNotebookStates: boolean,
connectionInfo: IResponse<IPhoenixServiceInfo>,
connectionStatus: DataModels.ContainerConnectionInfo,
): Promise<void> {
@@ -564,10 +536,10 @@ export default class Explorer {
forwardingId: connectionInfo.data.forwardingId,
dbAccountName: userContext.databaseAccount.name,
};
await this.phoenixClient.initiateContainerHeartBeat(shouldUseNotebookStates, containerData);
await this.phoenixClient.initiateContainerHeartBeat(true, containerData);
connectionStatus.status = ConnectionStatusType.Connected;
shouldUseNotebookStates && useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setConnectionInfo(connectionStatus);
const noteBookServerInfo = {
notebookServerEndpoint:
@@ -577,14 +549,11 @@ export default class Explorer {
authToken: userContext.features.notebookServerToken || connectionInfo.data.authToken,
forwardingId: connectionInfo.data.forwardingId,
};
shouldUseNotebookStates
? useNotebook.getState().setNotebookServerInfo(noteBookServerInfo)
: useQueryCopilot.getState().setNotebookServerInfo(noteBookServerInfo);
useNotebook.getState().setNotebookServerInfo(noteBookServerInfo);
shouldUseNotebookStates &&
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
}
private getDeltaDatabases(
@@ -780,7 +749,7 @@ export default class Explorer {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
}
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer(PoolIdType.DefaultPoolId);
await this.allocateContainer();
}
const notebookTabs = useTabs
@@ -1009,7 +978,7 @@ export default class Explorer {
if (userContext.features.enableCloudShell) {
this.connectToNotebookTerminal(kind);
} else if (useNotebook.getState().isPhoenixFeatures) {
await this.allocateContainer(PoolIdType.DefaultPoolId);
await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
this.connectToNotebookTerminal(kind);
@@ -1153,7 +1122,7 @@ export default class Explorer {
await useNotebook.getState().getPhoenixStatus();
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer(PoolIdType.DefaultPoolId);
await this.allocateContainer();
}
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
@@ -1207,23 +1176,38 @@ export default class Explorer {
scenarioMonitor.start(MetricScenario.DatabaseLoad);
}
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
if (userContext.authType === AuthType.ResourceToken) {
scenarioMonitor.skipPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.CollectionsLoaded);
scenarioMonitor.skipPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
this.databasesRefreshed = this.refreshDatabaseForResourceToken().then(() => {
scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
});
} else {
this.databasesRefreshed = this.refreshAllDatabases();
}
await this.databasesRefreshed; // await: we rely on the databases to be loaded before restoring the tabs further in the flow
}
// Run independent initialization tasks in parallel:
// - Database loading (ARM/SDK calls for databases + collections)
// - Notebook enabled check (Phoenix + Portal backend — no dependency on databases)
// - Feature registration check (ARM call — no dependency on databases or notebooks)
const databasesTask =
userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo"
? (async () => {
if (userContext.authType === AuthType.ResourceToken) {
scenarioMonitor.skipPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.CollectionsLoaded);
scenarioMonitor.skipPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
this.databasesRefreshed = this.refreshDatabaseForResourceToken().then(() => {
scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
});
} else {
this.databasesRefreshed = this.refreshAllDatabases();
}
await this.databasesRefreshed;
})()
: Promise.resolve();
if (!isFabricNative()) {
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
}
const notebooksTask = !isFabricNative()
? useNotebook.getState().refreshNotebooksEnabledStateForAccount()
: Promise.resolve();
const featureRegistrationTask =
userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()
? featureRegistered(userContext.subscriptionId, "ThroughputBucketing")
: Promise.resolve(false);
const [, , throughputBucketsEnabled] = await Promise.all([databasesTask, notebooksTask, featureRegistrationTask]);
// Notebook initialization depends on refreshNotebooksEnabledStateForAccount completing above
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled =
configContext.platform !== Platform.Fabric &&
@@ -1244,64 +1228,8 @@ export default class Explorer {
await this.initNotebooks(userContext.databaseAccount);
}
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()) {
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
if (throughputBucketsEnabled) {
updateUserContext({ throughputBucketsEnabled });
}
this.refreshSampleData();
}
public async configureCopilot(): Promise<void> {
if (
userContext.apiType !== "SQL" ||
!userContext.subscriptionId ||
![Environment.Development, Environment.Mpac, Environment.Prod].includes(getEnvironment())
) {
return;
}
const copilotEnabledPromise = getCopilotEnabled();
const copilotUserDBEnabledPromise = isCopilotFeatureRegistered(userContext.subscriptionId);
const [copilotEnabled, copilotUserDBEnabled] = await Promise.all([
copilotEnabledPromise,
copilotUserDBEnabledPromise,
]);
const copilotSampleDBEnabled = LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true";
useQueryCopilot.getState().setCopilotEnabled(copilotEnabled && copilotUserDBEnabled);
useQueryCopilot.getState().setCopilotUserDBEnabled(copilotUserDBEnabled);
useQueryCopilot
.getState()
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
}
public refreshSampleData(): void {
if (!userContext.sampleDataConnectionInfo) {
return;
}
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
if (!databaseId) {
return;
}
const startKey = TelemetryProcessor.traceStart(Action.RefreshSampleData, {
dataExplorerArea: Constants.Areas.ResourceTree,
databaseId,
});
readSampleCollection()
.then((collection: DataModels.Collection) => {
if (!collection) {
TelemetryProcessor.traceSuccess(Action.RefreshSampleData, { sampleCollectionFound: false }, startKey);
return;
}
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
useDatabases.setState({ sampleDataResourceTokenCollection });
TelemetryProcessor.traceSuccess(Action.RefreshSampleData, { sampleCollectionFound: true }, startKey);
})
.catch((error) => {
TelemetryProcessor.traceFailure(Action.RefreshSampleData, { error: getErrorMessage(error) }, startKey);
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData");
});
}
}

View File

@@ -12,7 +12,7 @@ import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { ConnectionStatusType } from "../../../Common/Constants";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
@@ -136,9 +136,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
// Add connection status if needed (using the hook values we got at the top level)
if ((isPhoenixNotebooks || isPhoenixFeatures) && connectionInfo?.status !== ConnectionStatusType.Connect) {
uiFabricControlButtons.unshift(
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"),
);
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
}
const rootStyle = {

View File

@@ -29,7 +29,7 @@ import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPa
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane, useDataPlaneRbac } from "../../Panes/SettingsPane/SettingsPane";
import { useDatabases } from "../../useDatabases";
import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
import { SelectedNodeState } from "../../useSelectedNode";
import { ThemeToggleButton } from "./ThemeToggleButton";
let counter = 0;
@@ -103,9 +103,7 @@ export function createStaticCommandBarButtons(
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState);
@@ -260,8 +258,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() || useNotebook.getState().isSynapseLinkUpdating,
disabled: useNotebook.getState().isSynapseLinkUpdating,
ariaLabel: label,
};
}
@@ -360,9 +357,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
styles: {
root: {
backgroundColor: "var(--colorNeutralBackground1)",
@@ -396,9 +391,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
styles: {
root: {
backgroundColor: "var(--colorNeutralBackground1)",
@@ -432,9 +425,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
styles: {
root: {
backgroundColor: "var(--colorNeutralBackground1)",
@@ -469,7 +460,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
disabled: false,
};
}
@@ -483,7 +474,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
disabled: false,
};
}

View File

@@ -7,12 +7,11 @@ import {
IDropdownStyles,
TooltipHost,
} from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { KeyboardHandlerMap } from "KeyboardShortcuts";
import * as React from "react";
import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { configContext, Platform } from "../../../ConfigContext";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
@@ -64,11 +63,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
onClick: btn.onCommandClick
? (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
btn.onCommandClick(ev);
let copilotEnabled = false;
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
}
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label });
}
: undefined,
key: `${btn.commandButtonLabel}${index}`,
@@ -270,10 +265,10 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
};
};
export const createConnectionStatus = (container: Explorer, poolId: PoolIdType, key: string): ICommandBarItemProps => {
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
onRender: () => <ConnectionStatus container={container} />,
};
};

View File

@@ -14,15 +14,14 @@ import { useId } from "@fluentui/react-hooks";
import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button";
import * as React from "react";
import "../../../../less/hostedexplorer.less";
import { ConnectionStatusType, ContainerStatusType, Notebook, PoolIdType } from "../../../Common/Constants";
import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less";
interface Props {
container: Explorer;
poolId: PoolIdType;
}
export const ConnectionStatus: React.FC<Props> = ({ container, poolId }: Props): JSX.Element => {
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
const connectionInfo = useNotebook((state) => state.connectionInfo);
const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00");
@@ -94,7 +93,7 @@ export const ConnectionStatus: React.FC<Props> = ({ container, poolId }: Props):
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
) {
return (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer(poolId)}>
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
@@ -134,9 +133,7 @@ export const ConnectionStatus: React.FC<Props> = ({ container, poolId }: Props):
id={buttonId}
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed
? container.allocateContainer(poolId)
: e.preventDefault()
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
}
>
<Stack className="connectionStatusContainer" horizontal>

View File

@@ -1,10 +1,10 @@
import { IDropdownOption } from "@fluentui/react";
import { Keys, t } from "Localization";
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants";
import { HttpStatusCodes } from "../../../Common/Constants";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import { Keys, t } from "Localization";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
@@ -110,7 +110,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
};
isGithubTree = false;
if (useNotebook.getState().isPhoenixNotebooks) {
await container.allocateContainer(PoolIdType.DefaultPoolId);
await container.allocateContainer();
}
break;

View File

@@ -23,7 +23,6 @@ import { SplitterDirection } from "Common/Splitter";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog";
import { useDatabases } from "Explorer/useDatabases";
import { Keys, t } from "Localization";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
import {
@@ -50,7 +49,6 @@ import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils"
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import create, { UseStore } from "zustand";
@@ -115,7 +113,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [refreshExplorer, setRefreshExplorer] = useState<boolean>(false);
const [refreshExplorer] = useState<boolean>(false);
const [pageOption, setPageOption] = useState<string>(
LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage
? Constants.Queries.UnlimitedPageOption
@@ -199,9 +197,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)
: Constants.PriorityLevel.Default,
);
const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState<boolean>(
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
);
const [mongoGuidRepresentation, setMongoGuidRepresentation] = useState<Constants.MongoGuidRepresentation>(
LocalStorageUtility.hasItem(StorageKey.MongoGuidRepresentation)
@@ -269,12 +264,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}
});
const shouldShowCopilotSampleDBOption =
userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled &&
useDatabases.getState().sampleDataResourceTokenCollection &&
!isEmulator;
const shouldShowMongoGuidRepresentationOption = userContext.apiType === "Mongo";
const handlerOnSubmit = async () => {
@@ -402,7 +391,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.setEntryString(StorageKey.QueryControlEnabled, queryControlEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.DefaultQueryResultsView, defaultQueryResultsView);
if (shouldShowGraphAutoVizOption) {
@@ -594,12 +582,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}
};
const handleSampleDatabaseChange = async (ev: React.MouseEvent<HTMLElement>, checked?: boolean): Promise<void> => {
setCopilotSampleDBEnabled(checked);
useQueryCopilot.getState().setCopilotSampleDBEnabled(checked);
setRefreshExplorer(false);
};
const handleOnMongoGuidRepresentationOptionChange = (
ev: React.FormEvent<HTMLInputElement>,
option: IDropdownOption,
@@ -1194,34 +1176,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{shouldShowCopilotSampleDBOption && (
<AccordionItem value="13">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.enableSampleDatabase)}</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.enableSampleDatabaseDescription)}
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel={t(Keys.panes.settings.enableSampleDbAriaLabel)}
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableSampleDatabase)}
</span>
)}
/>
</div>
</AccordionPanel>
</AccordionItem>
)}
{shouldShowMongoGuidRepresentationOption && (
<AccordionItem value="14">
<AccordionHeader>

View File

@@ -1,11 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../Explorer";
import { QueryCopilotCarousel } from "./CopilotCarousel";
describe("Query Copilot Carousel snapshot test", () => {
it("should render when isOpen is true", () => {
const wrapper = shallow(<QueryCopilotCarousel isOpen={true} explorer={new Explorer()} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,278 +0,0 @@
import {
DefaultButton,
ISeparatorStyles,
IconButton,
Image,
Link,
Modal,
PrimaryButton,
Separator,
Spinner,
Stack,
Text,
} from "@fluentui/react";
import { QueryCopilotSampleDatabaseId } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { StyleConstants } from "Common/StyleConstants";
import { createCollection } from "Common/dataAccess/createCollection";
import * as DataModels from "Contracts/DataModels";
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
import Explorer from "Explorer/Explorer";
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
import { useDatabases } from "Explorer/useDatabases";
import { useCarousel } from "hooks/useCarousel";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import React, { useState } from "react";
import YoutubePlaceholder from "../../../images/YoutubePlaceholder.svg";
interface QueryCopilotCarouselProps {
isOpen: boolean;
explorer: Explorer;
}
const separatorStyles: Partial<ISeparatorStyles> = {
root: {
selectors: {
"::before": {
background: StyleConstants.BaseMedium,
},
},
padding: "16px 0",
height: 1,
},
};
export const QueryCopilotCarousel: React.FC<QueryCopilotCarouselProps> = ({
isOpen,
explorer,
}: QueryCopilotCarouselProps): JSX.Element => {
const [page, setPage] = useState<number>(1);
const [isCreatingDatabase, setIsCreatingDatabase] = useState<boolean>(false);
const [spinnerText, setSpinnerText] = useState<string>("");
const [selectedPrompt, setSelectedPrompt] = useState<number>(1);
const getHeaderText = (): string => {
switch (page) {
case 1:
return "What exactly is copilot?";
case 2:
return "Setting up your Sample database";
case 3:
return "Sample prompts to help you";
default:
return "";
}
};
const getQueryCopilotInitialInput = (): string => {
switch (selectedPrompt) {
case 1:
return "Write a query to return all records in this table";
case 2:
return "Write a query to return all records in this table created in the last thirty days";
case 3:
return 'Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"';
default:
return "";
}
};
const createSampleDatabase = async (): Promise<void> => {
const database = useDatabases.getState().findDatabaseWithId(QueryCopilotSampleDatabaseId);
if (database) {
return;
}
try {
setIsCreatingDatabase(true);
setSpinnerText("Setting up your database...");
const params: DataModels.CreateCollectionParams = {
createNewDatabase: true,
collectionId: "SampleContainer",
databaseId: QueryCopilotSampleDatabaseId,
databaseLevelThroughput: true,
autoPilotMaxThroughput: 1000,
offerThroughput: undefined,
indexingPolicy: AllPropertiesIndexed,
partitionKey: {
paths: ["/categoryId"],
kind: "Hash",
version: 2,
},
};
await createCollection(params);
await explorer.refreshAllDatabases();
const database = useDatabases.getState().findDatabaseWithId(QueryCopilotSampleDatabaseId);
await database.loadCollections();
const collection = database.findCollectionWithId("SampleContainer");
setSpinnerText("Adding sample data set...");
const sampleGenerator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorer, true);
await sampleGenerator.populateContainerAsync(collection);
await database.expandDatabase();
collection.expandCollection();
useDatabases.getState().updateDatabase(database);
} catch (error) {
//TODO: show error in UI
handleError(error, "QueryCopilotCreateSampleDB");
throw error;
} finally {
setIsCreatingDatabase(false);
setSpinnerText("");
}
};
const getContent = (): JSX.Element => {
switch (page) {
case 1:
return (
<Stack style={{ marginTop: 8 }}>
<Text style={{ fontSize: 13 }}>
A couple of lines about copilot and the background about it. The idea is to have some text to give context
to the user.
</Text>
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>How do you use copilot</Text>
<Text style={{ fontSize: 13, marginTop: 8 }}>
To generate queries , just describe the query you want and copilot will generate the query for you.Watch
this video to learn more about how to use copilot.
</Text>
<Image src={YoutubePlaceholder} style={{ margin: "16px auto" }} />
<Text style={{ fontSize: 14, fontWeight: 600 }}>What is copilot good at</Text>
<Text style={{ fontSize: 13, marginTop: 8 }}>
A couple of lines about what copilot can do and its capablites with a link to{" "}
<Link href="" target="_blank">
documentation
</Link>{" "}
if possible.
</Text>
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>What are its limitations</Text>
<Text style={{ fontSize: 13, marginTop: 8 }}>
A couple of lines about what copilot cant do and its limitations.{" "}
<Link href="" target="_blank">
Link to documentation
</Link>
</Text>
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>Disclaimer</Text>
<Text style={{ fontSize: 13, marginTop: 8 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="" target="_blank">
Read preview terms
</Link>
</Text>
</Stack>
);
case 2:
return (
<Stack style={{ marginTop: 8 }}>
<Text style={{ fontSize: 13 }}>
Before you get started, we need to configure your sample database for you. Here is a summary of the
database being created for your reference. Configuration values can be updated using the settings icon in
the query builder.
</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
<Text style={{ fontSize: 13 }}>CopilotSampleDB</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
<Text style={{ fontSize: 13 }}>Autoscale</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
<Text>1000</Text>
<Text style={{ fontSize: 10, marginTop: 8 }}>
Your database throughput will automatically scale from{" "}
<strong>100 RU/s (10% of max RU/s) - 1000 RU/s</strong> based on usage.
</Text>
<Text style={{ fontSize: 10, marginTop: 8 }}>
Estimated monthly cost (USD): <strong>$8.76 - $87.60</strong> (1 region, 100 - 1000 RU/s, $0.00012/RU)
</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Container Id</Text>
<Text style={{ fontSize: 13 }}>SampleContainer</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Partition key</Text>
<Text style={{ fontSize: 13 }}>categoryId</Text>
</Stack>
);
case 3:
return (
<Stack>
<Text>To help you get started, here are some sample prompts to get you started</Text>
<Stack tokens={{ childrenGap: 12 }} style={{ marginTop: 16 }}>
<PromptCard
header="Write a query to return all records in this table"
description="This is a basic query which returns all records in the table "
onSelect={() => setSelectedPrompt(1)}
isSelected={selectedPrompt === 1}
/>
<PromptCard
header="Write a query to return all records in this table created in the last thirty days"
description="This builds on the previous query which returns all records in the table which were inserted in the last thirty days. You can also modify this query to return records based upon creation date"
onSelect={() => setSelectedPrompt(2)}
isSelected={selectedPrompt === 2}
/>
<PromptCard
header='Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"'
description='This builds on the previous query which returns all records in the table which were inserted in the last thirty days but which has the record owner as "contoso"'
onSelect={() => setSelectedPrompt(3)}
isSelected={selectedPrompt === 3}
/>
</Stack>
<Text style={{ fontSize: 13, marginTop: 32 }}>
Interested in learning more about how to write effective prompts. Please read this article for more
information.
</Text>
<Text style={{ fontSize: 13, marginTop: 16 }}>
You can also access these prompts by selecting the Samples prompts button in the query builder page.
</Text>
<Text style={{ fontSize: 13, marginTop: 16 }}>
Don&apos;t like any of the prompts? Just click Get Started and write your own prompt.
</Text>
</Stack>
);
default:
return <></>;
}
};
return (
<Modal styles={{ main: { width: 880 } }} isOpen={isOpen && page < 4}>
<Stack style={{ padding: 16 }}>
<Stack horizontal horizontalAlign="space-between">
<Text variant="xLarge">{getHeaderText()}</Text>
<IconButton
iconProps={{ iconName: "Cancel" }}
onClick={() => useCarousel.getState().setShowCopilotCarousel(false)}
/>
</Stack>
{getContent()}
<Separator styles={separatorStyles} />
<Stack horizontal horizontalAlign="start" verticalAlign="center">
{page !== 1 && (
<DefaultButton
text="Previous"
style={{ marginRight: 8 }}
onClick={() => setPage(page - 1)}
disabled={isCreatingDatabase}
/>
)}
<PrimaryButton
text={page === 3 ? "Get started" : "Next"}
onClick={async () => {
if (page === 3) {
useCarousel.getState().setShowCopilotCarousel(false);
useTabs.getState().setQueryCopilotTabInitialInput(getQueryCopilotInitialInput());
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
return;
}
if (page === 2) {
await createSampleDatabase();
}
setPage(page + 1);
}}
disabled={isCreatingDatabase}
/>
{isCreatingDatabase && <Spinner style={{ marginLeft: 8 }} />}
{isCreatingDatabase && <Text style={{ marginLeft: 8, color: "#0078D4" }}>{spinnerText}</Text>}
</Stack>
</Stack>
</Modal>
);
};

View File

@@ -1,192 +0,0 @@
import { Checkbox, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { getUserEmail } from "Utils/UserUtils";
import { shallow } from "enzyme";
import React from "react";
jest.mock("Utils/UserUtils");
(getUserEmail as jest.Mock).mockResolvedValue("test@email.com");
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
SubmitFeedback as jest.Mock;
jest.mock("Explorer/QueryCopilot/QueryCopilotContext");
const mockUseCopilotStore = useCopilotStore as jest.Mock;
const mockReturnValue = {
generatedQuery: "test query",
userPrompt: "test prompt",
likeQuery: false,
showFeedbackModal: false,
closeFeedbackModal: jest.fn,
setHideFeedbackModalForLikedQueries: jest.fn,
};
describe("Query Copilot Feedback Modal snapshot test", () => {
beforeEach(() => {
mockUseCopilotStore.mockReturnValue(mockReturnValue);
jest.clearAllMocks();
});
it("shoud render and match snapshot", () => {
mockUseCopilotStore.mockReturnValue({
...mockReturnValue,
showFeedbackModal: true,
});
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
expect(wrapper.props().isOpen).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
it("should close on cancel click", () => {
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const cancelButton = wrapper.find(IconButton);
cancelButton.simulate("click");
wrapper.setProps({});
expect(wrapper.props().isOpen).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
it("should get user unput", () => {
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const testUserInput = "test user input";
const userInput = wrapper.find(TextField).first();
userInput.simulate("change", {}, testUserInput);
expect(wrapper.find(TextField).first().props().value).toEqual(testUserInput);
expect(wrapper).toMatchSnapshot();
});
it("should not render dont show again button", () => {
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const dontShowAgain = wrapper.find(Checkbox);
expect(dontShowAgain).toHaveLength(0);
expect(wrapper).toMatchSnapshot();
});
it("should render dont show again button and check it", () => {
mockUseCopilotStore.mockReturnValue({
...mockReturnValue,
showFeedbackModal: true,
likeQuery: true,
});
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const dontShowAgain = wrapper.find(Checkbox);
dontShowAgain.simulate("change", {}, true);
expect(wrapper.find(Checkbox)).toHaveLength(1);
expect(wrapper.find(Checkbox).first().props().checked).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
it("should cancel submission", () => {
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const cancelButton = wrapper.find(DefaultButton);
cancelButton.simulate("click");
wrapper.setProps({});
expect(wrapper.props().isOpen).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
it("should not submit submission if required description field is null", () => {
const explorer = new Explorer();
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={explorer}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const submitButton = wrapper.find(PrimaryButton);
submitButton.simulate("click");
wrapper.setProps({});
expect(SubmitFeedback).toHaveBeenCalledTimes(0);
});
it("should submit submission", () => {
const explorer = new Explorer();
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={explorer}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const submitButton = wrapper.find("form");
submitButton.simulate("submit");
wrapper.setProps({});
expect(SubmitFeedback).toHaveBeenCalledTimes(1);
expect(SubmitFeedback).toHaveBeenCalledWith({
containerId: "CopilotUserContainer",
databaseId: "CopilotUserDb",
mode: "User",
params: {
likeQuery: false,
generatedQuery: "test query",
userPrompt: "test prompt",
description: "",
},
explorer: explorer,
});
expect(wrapper.props().isOpen).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,112 +0,0 @@
import {
Checkbox,
DefaultButton,
IconButton,
Link,
Modal,
PrimaryButton,
Stack,
Text,
TextField,
} from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import React from "react";
export const QueryCopilotFeedbackModal = ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): JSX.Element => {
const {
generatedQuery,
userPrompt,
likeQuery,
showFeedbackModal,
closeFeedbackModal,
setHideFeedbackModalForLikedQueries,
} = useCopilotStore();
const [description, setDescription] = React.useState<string>("");
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
const handleSubmit = () => {
closeFeedbackModal();
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
SubmitFeedback({
params: { generatedQuery, likeQuery, description, userPrompt },
explorer,
databaseId,
containerId,
mode: mode,
});
};
return (
<Modal isOpen={showFeedbackModal} styles={{ main: { borderRadius: 8, maxWidth: 600 } }}>
<form onSubmit={handleSubmit}>
<Stack style={{ padding: 24 }}>
<Stack horizontal horizontalAlign="space-between">
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
</Stack>
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
<TextField
styles={{ root: { marginBottom: 14 } }}
label="Description"
required
placeholder="Provide more details"
value={description}
onChange={(_, newValue) => setDescription(newValue)}
multiline
rows={3}
/>
<TextField
styles={{
root: { marginBottom: 14 },
fieldGroup: { backgroundColor: "#F3F2F1", borderRadius: 4, borderColor: "#D1D1D1" },
}}
label="Query generated"
defaultValue={generatedQuery}
multiline
rows={3}
readOnly
/>
<Text style={{ fontSize: 12, marginBottom: 14 }}>
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to
improve your and your organizations experience with this product. If you have any questions about the use
of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the
Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the
feedback you submit is considered Personal Data under that addendum. Please see the{" "}
{
<Link href="https://go.microsoft.com/fwlink/?LinkId=521839" 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>
</form>
</Modal>
);
};

View File

@@ -1,46 +0,0 @@
.modalContentPadding {
padding-top: 15px;
width: 513px;
}
.exitPadding {
padding: 0px 7px 0px 0px;
}
.previewMargin {
margin: 8px 10px 0px 0;
}
.preview {
padding: 0px 4px;
background: #f0f0f0;
border-radius: 8px;
font-size: 10px;
}
.exitIcon {
margin: 3px 7px 0px 0px;
color: #424242;
}
.text {
width: 348px;
padding: 8px 16px 8px 16px;
margin-left: 25px;
}
.bold {
font-weight: 600;
}
.imageTextPadding {
padding: 0px 5px 0px 0px;
}
.buttonPadding {
padding: 15px 0px 15px 0px;
}
.tryButton {
border-radius: 4px;
}

View File

@@ -1,32 +0,0 @@
import { shallow } from "enzyme";
import { withHooks } from "jest-react-hooks-shallow";
import React from "react";
import { WelcomeModal } from "./WelcomeModal";
describe("Query Copilot Welcome Modal snapshot test", () => {
it("should render when isOpen is true", () => {
withHooks(() => {
const spy = jest.spyOn(localStorage, "setItem");
spy.mockClear();
const wrapper = shallow(<WelcomeModal visible={true} />);
expect(wrapper.props().children.props.isOpen).toBeTruthy();
expect(wrapper).toMatchSnapshot();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith("hideWelcomeModal", "true");
});
});
it("should not render when isOpen is false", () => {
withHooks(() => {
const spy = jest.spyOn(localStorage, "setItem");
spy.mockClear();
const wrapper = shallow(<WelcomeModal visible={false} />);
expect(wrapper.props().children.props.isOpen).toBeFalsy();
expect(spy).not.toHaveBeenCalled();
expect(spy.mock.instances.length).toBe(0);
});
});
});

View File

@@ -1,110 +0,0 @@
import { IconButton, Image, Link, Modal, PrimaryButton, Stack, StackItem, Text } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React from "react";
import Flash from "../../../../images/CopilotFlash.svg";
import Thumb from "../../../../images/CopilotThumb.svg";
import CoplilotWelcomeIllustration from "../../../../images/CopliotWelcomeIllustration.svg";
import "./WelcomeModal.css";
export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element => {
const [isModalVisible, { setFalse: hideModal }] = useBoolean(visible);
React.useEffect(() => {
if (visible) {
window.localStorage.setItem("hideWelcomeModal", "true");
}
});
return (
<>
<Modal
isOpen={isModalVisible}
onDismiss={hideModal}
isBlocking={false}
styles={{
main: {
maxHeight: 600,
borderRadius: 10,
overflow: "hidden",
},
}}
>
<Stack className="modalContentPadding">
<Stack horizontal>
<Stack horizontal grow={4} horizontalAlign="end">
<Stack.Item>
<Image src={CoplilotWelcomeIllustration} />
</Stack.Item>
</Stack>
<Stack horizontal grow={1} horizontalAlign="end" verticalAlign="start" className="exitPadding">
<Stack.Item>
<IconButton
onClick={hideModal}
iconProps={{ iconName: "Cancel" }}
title="Exit"
ariaLabel="Exit"
className="exitIcon"
/>
</Stack.Item>
</Stack>
</Stack>
<Stack horizontalAlign="center">
<Stack.Item align="center" style={{ textAlign: "center" }}>
<Text className="title bold" as={"h1"}>
Welcome to Microsoft Copilot for Azure in Cosmos DB (preview)
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
<Stack horizontal>
<StackItem align="start" className="imageTextPadding">
<Image src={Flash} />
</StackItem>
<StackItem align="start">
<Text className="bold">
Let Copilot do the work for you
<br />
</Text>
</StackItem>
</Stack>
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<Link target="_blank" href="https://aka.ms/MicrosoftCopilotForAzureInCDBHowTo">
Learn more
</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
<Stack horizontal>
<StackItem align="start" className="imageTextPadding">
<Image src={Thumb} />
</StackItem>
<StackItem align="start">
<Text className="bold">
Use your judgement
<br />
</Text>
</StackItem>
</Stack>
<Text>
AI-generated content can have mistakes. Make sure it is accurate and appropriate before executing the
query.
<br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-preview-terms">
Read our preview terms here
</Link>
</Text>
</Stack.Item>
</Stack>
<Stack className="buttonPadding">
<Stack.Item align="center">
<PrimaryButton onClick={hideModal} className="tryButton">
Try Copilot
</PrimaryButton>
</Stack.Item>
</Stack>
</Stack>
</Modal>
</>
);
};

View File

@@ -1,975 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Query Copilot Feedback Modal snapshot test shoud render and match snapshot 1`] = `
<Modal
isOpen={true}
styles={
{
"main": {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
>
<Stack
style={
{
"padding": 24,
}
}
>
<Stack
horizontal={true}
horizontalAlign="space-between"
>
<Text
style={
{
"fontSize": 20,
"fontWeight": 600,
"marginBottom": 20,
}
}
>
Send feedback to Microsoft
</Text>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
/>
</Stack>
<Text
style={
{
"fontSize": 14,
"marginBottom": 14,
}
}
>
Your feedback will help improve the experience.
</Text>
<StyledTextFieldBase
label="Description"
multiline={true}
onChange={[Function]}
placeholder="Provide more details"
required={true}
rows={3}
styles={
{
"root": {
"marginBottom": 14,
},
}
}
value=""
/>
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
{
"fieldGroup": {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": {
"marginBottom": 14,
},
}
}
/>
<Text
style={
{
"fontSize": 12,
"marginBottom": 14,
}
}
>
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
<StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839"
target="_blank"
>
Privacy statement
</StyledLinkBase>
for more information.
</Text>
<Stack
horizontal={true}
horizontalAlign="end"
>
<CustomizedPrimaryButton
styles={
{
"root": {
"marginRight": 8,
},
}
}
type="submit"
>
Submit
</CustomizedPrimaryButton>
<CustomizedDefaultButton
onClick={[Function]}
>
Cancel
</CustomizedDefaultButton>
</Stack>
</Stack>
</form>
</Modal>
`;
exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`] = `
<Modal
isOpen={false}
styles={
{
"main": {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
>
<Stack
style={
{
"padding": 24,
}
}
>
<Stack
horizontal={true}
horizontalAlign="space-between"
>
<Text
style={
{
"fontSize": 20,
"fontWeight": 600,
"marginBottom": 20,
}
}
>
Send feedback to Microsoft
</Text>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
/>
</Stack>
<Text
style={
{
"fontSize": 14,
"marginBottom": 14,
}
}
>
Your feedback will help improve the experience.
</Text>
<StyledTextFieldBase
label="Description"
multiline={true}
onChange={[Function]}
placeholder="Provide more details"
required={true}
rows={3}
styles={
{
"root": {
"marginBottom": 14,
},
}
}
value=""
/>
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
{
"fieldGroup": {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": {
"marginBottom": 14,
},
}
}
/>
<Text
style={
{
"fontSize": 12,
"marginBottom": 14,
}
}
>
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
<StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839"
target="_blank"
>
Privacy statement
</StyledLinkBase>
for more information.
</Text>
<Stack
horizontal={true}
horizontalAlign="end"
>
<CustomizedPrimaryButton
styles={
{
"root": {
"marginRight": 8,
},
}
}
type="submit"
>
Submit
</CustomizedPrimaryButton>
<CustomizedDefaultButton
onClick={[Function]}
>
Cancel
</CustomizedDefaultButton>
</Stack>
</Stack>
</form>
</Modal>
`;
exports[`Query Copilot Feedback Modal snapshot test should close on cancel click 1`] = `
<Modal
isOpen={false}
styles={
{
"main": {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
>
<Stack
style={
{
"padding": 24,
}
}
>
<Stack
horizontal={true}
horizontalAlign="space-between"
>
<Text
style={
{
"fontSize": 20,
"fontWeight": 600,
"marginBottom": 20,
}
}
>
Send feedback to Microsoft
</Text>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
/>
</Stack>
<Text
style={
{
"fontSize": 14,
"marginBottom": 14,
}
}
>
Your feedback will help improve the experience.
</Text>
<StyledTextFieldBase
label="Description"
multiline={true}
onChange={[Function]}
placeholder="Provide more details"
required={true}
rows={3}
styles={
{
"root": {
"marginBottom": 14,
},
}
}
value=""
/>
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
{
"fieldGroup": {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": {
"marginBottom": 14,
},
}
}
/>
<Text
style={
{
"fontSize": 12,
"marginBottom": 14,
}
}
>
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
<StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839"
target="_blank"
>
Privacy statement
</StyledLinkBase>
for more information.
</Text>
<Stack
horizontal={true}
horizontalAlign="end"
>
<CustomizedPrimaryButton
styles={
{
"root": {
"marginRight": 8,
},
}
}
type="submit"
>
Submit
</CustomizedPrimaryButton>
<CustomizedDefaultButton
onClick={[Function]}
>
Cancel
</CustomizedDefaultButton>
</Stack>
</Stack>
</form>
</Modal>
`;
exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] = `
<Modal
isOpen={false}
styles={
{
"main": {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
>
<Stack
style={
{
"padding": 24,
}
}
>
<Stack
horizontal={true}
horizontalAlign="space-between"
>
<Text
style={
{
"fontSize": 20,
"fontWeight": 600,
"marginBottom": 20,
}
}
>
Send feedback to Microsoft
</Text>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
/>
</Stack>
<Text
style={
{
"fontSize": 14,
"marginBottom": 14,
}
}
>
Your feedback will help improve the experience.
</Text>
<StyledTextFieldBase
label="Description"
multiline={true}
onChange={[Function]}
placeholder="Provide more details"
required={true}
rows={3}
styles={
{
"root": {
"marginBottom": 14,
},
}
}
value="test user input"
/>
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
{
"fieldGroup": {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": {
"marginBottom": 14,
},
}
}
/>
<Text
style={
{
"fontSize": 12,
"marginBottom": 14,
}
}
>
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
<StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839"
target="_blank"
>
Privacy statement
</StyledLinkBase>
for more information.
</Text>
<Stack
horizontal={true}
horizontalAlign="end"
>
<CustomizedPrimaryButton
styles={
{
"root": {
"marginRight": 8,
},
}
}
type="submit"
>
Submit
</CustomizedPrimaryButton>
<CustomizedDefaultButton
onClick={[Function]}
>
Cancel
</CustomizedDefaultButton>
</Stack>
</Stack>
</form>
</Modal>
`;
exports[`Query Copilot Feedback Modal snapshot test should not render dont show again button 1`] = `
<Modal
isOpen={false}
styles={
{
"main": {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
>
<Stack
style={
{
"padding": 24,
}
}
>
<Stack
horizontal={true}
horizontalAlign="space-between"
>
<Text
style={
{
"fontSize": 20,
"fontWeight": 600,
"marginBottom": 20,
}
}
>
Send feedback to Microsoft
</Text>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
/>
</Stack>
<Text
style={
{
"fontSize": 14,
"marginBottom": 14,
}
}
>
Your feedback will help improve the experience.
</Text>
<StyledTextFieldBase
label="Description"
multiline={true}
onChange={[Function]}
placeholder="Provide more details"
required={true}
rows={3}
styles={
{
"root": {
"marginBottom": 14,
},
}
}
value=""
/>
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
{
"fieldGroup": {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": {
"marginBottom": 14,
},
}
}
/>
<Text
style={
{
"fontSize": 12,
"marginBottom": 14,
}
}
>
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
<StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839"
target="_blank"
>
Privacy statement
</StyledLinkBase>
for more information.
</Text>
<Stack
horizontal={true}
horizontalAlign="end"
>
<CustomizedPrimaryButton
styles={
{
"root": {
"marginRight": 8,
},
}
}
type="submit"
>
Submit
</CustomizedPrimaryButton>
<CustomizedDefaultButton
onClick={[Function]}
>
Cancel
</CustomizedDefaultButton>
</Stack>
</Stack>
</form>
</Modal>
`;
exports[`Query Copilot Feedback Modal snapshot test should render dont show again button and check it 1`] = `
<Modal
isOpen={true}
styles={
{
"main": {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
>
<Stack
style={
{
"padding": 24,
}
}
>
<Stack
horizontal={true}
horizontalAlign="space-between"
>
<Text
style={
{
"fontSize": 20,
"fontWeight": 600,
"marginBottom": 20,
}
}
>
Send feedback to Microsoft
</Text>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
/>
</Stack>
<Text
style={
{
"fontSize": 14,
"marginBottom": 14,
}
}
>
Your feedback will help improve the experience.
</Text>
<StyledTextFieldBase
label="Description"
multiline={true}
onChange={[Function]}
placeholder="Provide more details"
required={true}
rows={3}
styles={
{
"root": {
"marginBottom": 14,
},
}
}
value=""
/>
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
{
"fieldGroup": {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": {
"marginBottom": 14,
},
}
}
/>
<Text
style={
{
"fontSize": 12,
"marginBottom": 14,
}
}
>
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
<StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839"
target="_blank"
>
Privacy statement
</StyledLinkBase>
for more information.
</Text>
<StyledCheckboxBase
checked={true}
label="Don't show me this next time"
onChange={[Function]}
styles={
{
"label": {
"paddingLeft": 0,
},
"root": {
"marginBottom": 14,
},
}
}
/>
<Stack
horizontal={true}
horizontalAlign="end"
>
<CustomizedPrimaryButton
styles={
{
"root": {
"marginRight": 8,
},
}
}
type="submit"
>
Submit
</CustomizedPrimaryButton>
<CustomizedDefaultButton
onClick={[Function]}
>
Cancel
</CustomizedDefaultButton>
</Stack>
</Stack>
</form>
</Modal>
`;
exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`] = `
<Modal
isOpen={false}
styles={
{
"main": {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
>
<Stack
style={
{
"padding": 24,
}
}
>
<Stack
horizontal={true}
horizontalAlign="space-between"
>
<Text
style={
{
"fontSize": 20,
"fontWeight": 600,
"marginBottom": 20,
}
}
>
Send feedback to Microsoft
</Text>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
/>
</Stack>
<Text
style={
{
"fontSize": 14,
"marginBottom": 14,
}
}
>
Your feedback will help improve the experience.
</Text>
<StyledTextFieldBase
label="Description"
multiline={true}
onChange={[Function]}
placeholder="Provide more details"
required={true}
rows={3}
styles={
{
"root": {
"marginBottom": 14,
},
}
}
value=""
/>
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
{
"fieldGroup": {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": {
"marginBottom": 14,
},
}
}
/>
<Text
style={
{
"fontSize": 12,
"marginBottom": 14,
}
}
>
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
<StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839"
target="_blank"
>
Privacy statement
</StyledLinkBase>
for more information.
</Text>
<Stack
horizontal={true}
horizontalAlign="end"
>
<CustomizedPrimaryButton
styles={
{
"root": {
"marginRight": 8,
},
}
}
type="submit"
>
Submit
</CustomizedPrimaryButton>
<CustomizedDefaultButton
onClick={[Function]}
>
Cancel
</CustomizedDefaultButton>
</Stack>
</Stack>
</form>
</Modal>
`;

View File

@@ -1,168 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is true 1`] = `
<Fragment>
<Modal
isBlocking={false}
isOpen={true}
onDismiss={[Function]}
styles={
{
"main": {
"borderRadius": 10,
"maxHeight": 600,
"overflow": "hidden",
},
}
}
>
<Stack
className="modalContentPadding"
>
<Stack
horizontal={true}
>
<Stack
grow={4}
horizontal={true}
horizontalAlign="end"
>
<StackItem>
<Image
src={{}}
/>
</StackItem>
</Stack>
<Stack
className="exitPadding"
grow={1}
horizontal={true}
horizontalAlign="end"
verticalAlign="start"
>
<StackItem>
<CustomizedIconButton
ariaLabel="Exit"
className="exitIcon"
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
title="Exit"
/>
</StackItem>
</Stack>
</Stack>
<Stack
horizontalAlign="center"
>
<StackItem
align="center"
style={
{
"textAlign": "center",
}
}
>
<Text
as="h1"
className="title bold"
>
Welcome to Microsoft Copilot for Azure in Cosmos DB (preview)
</Text>
</StackItem>
<StackItem
align="center"
className="text"
>
<Stack
horizontal={true}
>
<StackItem
align="start"
className="imageTextPadding"
>
<Image
src={{}}
/>
</StackItem>
<StackItem
align="start"
>
<Text
className="bold"
>
Let Copilot do the work for you
<br />
</Text>
</StackItem>
</Stack>
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<StyledLinkBase
href="https://aka.ms/MicrosoftCopilotForAzureInCDBHowTo"
target="_blank"
>
Learn more
</StyledLinkBase>
</Text>
</StackItem>
<StackItem
align="center"
className="text"
>
<Stack
horizontal={true}
>
<StackItem
align="start"
className="imageTextPadding"
>
<Image
src={{}}
/>
</StackItem>
<StackItem
align="start"
>
<Text
className="bold"
>
Use your judgement
<br />
</Text>
</StackItem>
</Stack>
<Text>
AI-generated content can have mistakes. Make sure it is accurate and appropriate before executing the query.
<br />
<StyledLinkBase
href="https://aka.ms/cdb-copilot-preview-terms"
target="_blank"
>
Read our preview terms here
</StyledLinkBase>
</Text>
</StackItem>
</Stack>
<Stack
className="buttonPadding"
>
<StackItem
align="center"
>
<CustomizedPrimaryButton
className="tryButton"
onClick={[Function]}
>
Try Copilot
</CustomizedPrimaryButton>
</StackItem>
</Stack>
</Stack>
</Modal>
</Fragment>
`;

View File

@@ -1,48 +0,0 @@
import { IconButton } from "@fluentui/react";
import { shallow } from "enzyme";
import React from "react";
import { CopyPopup } from "./CopyPopup";
describe("Copy Popup snapshot test", () => {
const setShowCopyPopupMock = jest.fn();
it("should render when showCopyPopup is true", () => {
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
expect(wrapper.exists()).toBe(true);
expect(wrapper.prop("setShowCopyPopup")).toBeUndefined();
expect(wrapper).toMatchSnapshot();
});
it("should render when showCopyPopup is false", () => {
const wrapper = shallow(<CopyPopup showCopyPopup={false} setShowCopyPopup={setShowCopyPopupMock} />);
expect(wrapper.prop("showCopyPopup")).toBeFalsy();
expect(wrapper.prop("setShowCopyPopup")).toBeUndefined();
expect(wrapper).toMatchSnapshot();
});
it("should call setShowCopyPopup(false) when close button is clicked", () => {
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
const closeButton = wrapper.find(IconButton);
closeButton.props().onClick?.({} as React.MouseEvent<HTMLButtonElement, MouseEvent>);
expect(setShowCopyPopupMock).toHaveBeenCalledWith(false);
});
it("should have the correct inline styles", () => {
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
const stackStyle = wrapper.find("Stack").first().props().style;
expect(stackStyle).toEqual({
position: "fixed",
width: 345,
height: 66,
padding: 10,
gap: 5,
top: 75,
right: 20,
background: "#FFFFFF",
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.16)",
});
});
});

View File

@@ -1,64 +0,0 @@
import { IconButton, Image, Stack, Text } from "@fluentui/react";
import React, { Dispatch, SetStateAction } from "react";
import Success from "../../../../images/successfulPopup.svg";
export const CopyPopup = ({
showCopyPopup,
setShowCopyPopup,
}: {
showCopyPopup: boolean;
setShowCopyPopup: Dispatch<SetStateAction<boolean>>;
}): JSX.Element => {
const closePopup = () => {
setShowCopyPopup(false);
};
return showCopyPopup ? (
<Stack
role="status"
style={{
position: "fixed",
width: 345,
height: 66,
padding: 10,
gap: 5,
top: 75,
right: 20,
background: "#FFFFFF",
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.16)",
}}
>
<Stack
horizontal
verticalAlign="center"
style={{ display: "flex", justifyContent: "space-between", padding: "5px, 2px, 0px, 0px" }}
>
<Stack horizontal verticalAlign="center" style={{ display: "flex", gap: 10 }}>
<Image style={{ width: 15, height: 15 }} src={Success} />
<Text>
<b>Code copied successfully</b>
</Text>
</Stack>
<IconButton
styles={{
root: {
border: "none",
backgroundColor: "transparent",
padding: 0,
selectors: {
"&:focus": {
outline: "none",
},
},
},
}}
iconProps={{ iconName: "Cancel" }}
onClick={closePopup}
/>
</Stack>
<Text style={{ marginTop: -10 }}>The query has been copied to the clipboard</Text>
</Stack>
) : (
<></>
);
};

View File

@@ -1,113 +0,0 @@
import { mount, shallow } from "enzyme";
import React from "react";
import { DeletePopup } from "./DeletePopup";
describe("Delete Popup snapshot test", () => {
const setShowDeletePopupMock = jest.fn();
const setQueryMock = jest.fn();
const clearFeedbackMock = jest.fn();
const showFeedbackBarMock = jest.fn();
it("should render when showDeletePopup is true", () => {
const wrapper = shallow(
<DeletePopup
showDeletePopup={true}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>,
);
expect(wrapper.find("Modal").prop("isOpen")).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
it("should not render when showDeletePopup is false", () => {
const wrapper = shallow(
<DeletePopup
showDeletePopup={false}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>,
);
expect(wrapper.props().children.props.showDeletePopup).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
it("should call setQuery with an empty string and setShowDeletePopup(false) when delete button is clicked", () => {
const wrapper = mount(
<DeletePopup
showDeletePopup={true}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>,
);
wrapper.find("PrimaryButton").simulate("click");
expect(setQueryMock).toHaveBeenCalledWith("");
expect(setShowDeletePopupMock).toHaveBeenCalledWith(false);
});
it("should call setShowDeletePopup(false) when close button is clicked", () => {
const setShowDeletePopupMock = jest.fn();
const wrapper = mount(
<DeletePopup
showDeletePopup={true}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>,
);
wrapper.find("DefaultButton").at(1).simulate("click");
expect(setShowDeletePopupMock).toHaveBeenCalledWith(false);
});
it("should render the appropriate text content", () => {
const wrapper = shallow(
<DeletePopup
showDeletePopup={true}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>,
);
const textContent = wrapper
.find("Text")
.map((text, index) => <React.Fragment key={index}>{text.props().children}</React.Fragment>);
expect(textContent).toEqual([
<React.Fragment key={0}>
<b>Delete code?</b>
</React.Fragment>,
<React.Fragment key={1}>
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
</React.Fragment>,
]);
});
it("should have the correct inline style", () => {
const wrapper = shallow(
<DeletePopup
showDeletePopup={true}
setShowDeletePopup={setShowDeletePopupMock}
setQuery={setQueryMock}
clearFeedback={clearFeedbackMock}
showFeedbackBar={showFeedbackBarMock}
/>,
);
const stackStyle = wrapper.find("Stack[style]").props().style;
expect(stackStyle).toEqual({ padding: "16px 24px", height: "auto" });
});
});

View File

@@ -1,49 +0,0 @@
import { DefaultButton, Modal, PrimaryButton, Stack, Text } from "@fluentui/react";
import React, { Dispatch, SetStateAction } from "react";
export const DeletePopup = ({
showDeletePopup,
setShowDeletePopup,
setQuery,
clearFeedback,
showFeedbackBar,
}: {
showDeletePopup: boolean;
setShowDeletePopup: Dispatch<SetStateAction<boolean>>;
setQuery: Dispatch<SetStateAction<string>>;
clearFeedback: Dispatch<SetStateAction<void>>;
showFeedbackBar: Dispatch<SetStateAction<boolean>>;
}): JSX.Element => {
const deleteCode = () => {
setQuery("");
setShowDeletePopup(false);
clearFeedback();
showFeedbackBar(false);
};
return (
<Modal
isOpen={showDeletePopup}
styles={{ main: { minHeight: "122px", minWidth: "880px" } }}
titleAriaId="deleteDialogTitle"
subtitleAriaId="deleteDialogSubTitle"
>
<Stack style={{ padding: "16px 24px", height: "auto" }}>
<Text id="deleteDialogTitle" style={{ height: 24, fontSize: "18px" }}>
<b>Delete code?</b>
</Text>
<Text id="deleteDialogSubTitle" style={{ marginTop: 10, marginBottom: 20 }}>
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
</Text>
<Stack horizontal tokens={{ childrenGap: 10 }} horizontalAlign="start">
<PrimaryButton style={{ padding: "0px 20px", height: 24 }} onClick={deleteCode}>
Delete
</PrimaryButton>
<DefaultButton style={{ padding: "0px 20px", height: 24 }} onClick={() => setShowDeletePopup(false)}>
Close
</DefaultButton>
</Stack>
</Stack>
</Modal>
);
};

View File

@@ -1,91 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Copy Popup snapshot test should render when showCopyPopup is false 1`] = `<Fragment />`;
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
<Stack
role="status"
style={
{
"background": "#FFFFFF",
"boxShadow": "0 2px 6px rgba(0, 0, 0, 0.16)",
"gap": 5,
"height": 66,
"padding": 10,
"position": "fixed",
"right": 20,
"top": 75,
"width": 345,
}
}
>
<Stack
horizontal={true}
style={
{
"display": "flex",
"justifyContent": "space-between",
"padding": "5px, 2px, 0px, 0px",
}
}
verticalAlign="center"
>
<Stack
horizontal={true}
style={
{
"display": "flex",
"gap": 10,
}
}
verticalAlign="center"
>
<Image
src={{}}
style={
{
"height": 15,
"width": 15,
}
}
/>
<Text>
<b>
Code copied successfully
</b>
</Text>
</Stack>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
styles={
{
"root": {
"backgroundColor": "transparent",
"border": "none",
"padding": 0,
"selectors": {
"&:focus": {
"outline": "none",
},
},
},
}
}
/>
</Stack>
<Text
style={
{
"marginTop": -10,
}
}
>
The query has been copied to the clipboard
</Text>
</Stack>
`;

View File

@@ -1,165 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Delete Popup snapshot test should not render when showDeletePopup is false 1`] = `
<Modal
isOpen={false}
styles={
{
"main": {
"minHeight": "122px",
"minWidth": "880px",
},
}
}
subtitleAriaId="deleteDialogSubTitle"
titleAriaId="deleteDialogTitle"
>
<Stack
style={
{
"height": "auto",
"padding": "16px 24px",
}
}
>
<Text
id="deleteDialogTitle"
style={
{
"fontSize": "18px",
"height": 24,
}
}
>
<b>
Delete code?
</b>
</Text>
<Text
id="deleteDialogSubTitle"
style={
{
"marginBottom": 20,
"marginTop": 10,
}
}
>
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
</Text>
<Stack
horizontal={true}
horizontalAlign="start"
tokens={
{
"childrenGap": 10,
}
}
>
<CustomizedPrimaryButton
onClick={[Function]}
style={
{
"height": 24,
"padding": "0px 20px",
}
}
>
Delete
</CustomizedPrimaryButton>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"height": 24,
"padding": "0px 20px",
}
}
>
Close
</CustomizedDefaultButton>
</Stack>
</Stack>
</Modal>
`;
exports[`Delete Popup snapshot test should render when showDeletePopup is true 1`] = `
<Modal
isOpen={true}
styles={
{
"main": {
"minHeight": "122px",
"minWidth": "880px",
},
}
}
subtitleAriaId="deleteDialogSubTitle"
titleAriaId="deleteDialogTitle"
>
<Stack
style={
{
"height": "auto",
"padding": "16px 24px",
}
}
>
<Text
id="deleteDialogTitle"
style={
{
"fontSize": "18px",
"height": 24,
}
}
>
<b>
Delete code?
</b>
</Text>
<Text
id="deleteDialogSubTitle"
style={
{
"marginBottom": 20,
"marginTop": 10,
}
}
>
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
</Text>
<Stack
horizontal={true}
horizontalAlign="start"
tokens={
{
"childrenGap": 10,
}
}
>
<CustomizedPrimaryButton
onClick={[Function]}
style={
{
"height": 24,
"padding": "0px 20px",
}
}
>
Delete
</CustomizedPrimaryButton>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"height": 24,
"padding": "0px 20px",
}
}
>
Close
</CustomizedDefaultButton>
</Stack>
</Stack>
</Modal>
`;

View File

@@ -1,19 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { PromptCard } from "./PromptCard";
describe("Prompt card snapshot test", () => {
it("should render properly if isSelected is true", () => {
const wrapper = shallow(
<PromptCard header="TestHeader" description="TestDescription" isSelected={true} onSelect={() => undefined} />,
);
expect(wrapper).toMatchSnapshot();
});
it("should render properly if isSelected is false", () => {
const wrapper = shallow(
<PromptCard header="TestHeader" description="TestDescription" isSelected={false} onSelect={() => undefined} />,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,48 +0,0 @@
import { ChoiceGroup, Stack, Text } from "@fluentui/react";
import React from "react";
interface PromptCardProps {
header: string;
description: string;
isSelected: boolean;
onSelect: () => void;
}
export const PromptCard: React.FC<PromptCardProps> = ({
header,
description,
isSelected,
onSelect,
}: PromptCardProps): JSX.Element => {
return (
<Stack
horizontal
style={{
padding: "16px 0 16px 16px ",
boxSizing: "border-box",
width: 650,
height: 100,
border: "1px solid #F3F2F1",
boxShadow: "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
}}
>
<Stack.Item grow={1}>
<Stack horizontal>
<div>
<Text style={{ fontSize: 13, color: "#00A2AD", background: "#F8FFF0" }}>Prompt</Text>
</div>
<Text style={{ fontSize: 13, marginLeft: 16 }}>{header}</Text>
</Stack>
<Text style={{ fontSize: 10, marginTop: 16 }}>{description}</Text>
</Stack.Item>
<Stack.Item style={{ marginLeft: 16 }}>
<ChoiceGroup
styles={{ flexContainer: { width: 36 } }}
options={[{ key: "selected", text: "" }]}
selectedKey={isSelected ? "selected" : ""}
onChange={onSelect}
/>
</Stack.Item>
</Stack>
);
};

View File

@@ -1,139 +0,0 @@
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import QueryError from "Common/QueryError";
import { QueryResults } from "Contracts/ViewModels";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { guid } from "Explorer/Tables/Utilities";
import { QueryCopilotState } from "hooks/useQueryCopilot";
import React, { createContext, useContext, useState } from "react";
import create from "zustand";
const context = createContext(null);
const useCopilotStore = (): Partial<QueryCopilotState> => useContext(context);
const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Element => {
const [useStore] = useState(() =>
create((set, get) => ({
generatedQuery: "",
likeQuery: false,
userPrompt: "",
showFeedbackModal: false,
hideFeedbackModalForLikedQueries: false,
correlationId: "",
query: "SELECT * FROM c",
selectedQuery: "",
isGeneratingQuery: false,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errors: [],
isSamplePromptsOpen: false,
showPromptTeachingBubble: true,
showDeletePopup: false,
showFeedbackBar: false,
showCopyPopup: false,
showErrorMessageBar: false,
showInvalidQueryMessageBar: false,
generatedQueryComments: "",
wasCopilotUsed: false,
showWelcomeSidebar: true,
showCopilotSidebar: false,
chatMessages: [],
shouldIncludeInMessages: true,
showExplanationBubble: false,
isAllocatingContainer: false,
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ showFeedbackModal: false }),
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
set({ hideFeedbackModalForLikedQueries }),
refreshCorrelationId: () => set({ correlationId: guid() }),
setUserPrompt: (userPrompt: string) => set({ userPrompt }),
setQuery: (query: string) => set({ query }),
setGeneratedQuery: (generatedQuery: string) => set({ generatedQuery }),
setSelectedQuery: (selectedQuery: string) => set({ selectedQuery }),
setIsGeneratingQuery: (isGeneratingQuery: boolean) => set({ isGeneratingQuery }),
setIsGeneratingExplanation: (isGeneratingExplanation: boolean) => set({ isGeneratingExplanation }),
setIsExecuting: (isExecuting: boolean) => set({ isExecuting }),
setLikeQuery: (likeQuery: boolean) => set({ likeQuery }),
setDislikeQuery: (dislikeQuery: boolean | undefined) => set({ dislikeQuery }),
setShowCallout: (showCallout: boolean) => set({ showCallout }),
setShowSamplePrompts: (showSamplePrompts: boolean) => set({ showSamplePrompts }),
setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => set({ queryIterator }),
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
setErrors: (errors: QueryError[]) => set({ errors }),
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => set({ showPromptTeachingBubble }),
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
setShowErrorMessageBar: (showErrorMessageBar: boolean) => set({ showErrorMessageBar }),
setShowInvalidQueryMessageBar: (showInvalidQueryMessageBar: boolean) => set({ showInvalidQueryMessageBar }),
setGeneratedQueryComments: (generatedQueryComments: string) => set({ generatedQueryComments }),
setWasCopilotUsed: (wasCopilotUsed: boolean) => set({ wasCopilotUsed }),
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => set({ showWelcomeSidebar }),
setShowCopilotSidebar: (showCopilotSidebar: boolean) => set({ showCopilotSidebar }),
setChatMessages: (chatMessages: CopilotMessage[]) => set({ chatMessages }),
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }),
setShowExplanationBubble: (showExplanationBubble: boolean) => set({ showExplanationBubble }),
getState: () => {
return get();
},
resetQueryCopilotStates: () => {
set((state) => ({
...state,
generatedQuery: "",
likeQuery: false,
userPrompt: "",
showFeedbackModal: false,
hideFeedbackModalForLikedQueries: false,
correlationId: "",
query: "SELECT * FROM c",
selectedQuery: "",
isGeneratingQuery: false,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errorMessage: "",
isSamplePromptsOpen: false,
showPromptTeachingBubble: true,
showDeletePopup: false,
showFeedbackBar: false,
showCopyPopup: false,
showErrorMessageBar: false,
showInvalidQueryMessageBar: false,
generatedQueryComments: "",
wasCopilotUsed: false,
showCopilotSidebar: false,
chatMessages: [],
shouldIncludeInMessages: true,
showExplanationBubble: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
},
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
},
isAllocatingContainer: false,
}));
},
})),
);
return <context.Provider value={useStore()}>{children}</context.Provider>;
};
export { CopilotProvider, useCopilotStore };

View File

@@ -1,769 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
import {
Callout,
CommandBarButton,
DefaultButton,
DirectionalHint,
IButtonStyles,
IconButton,
Image,
Link,
MessageBar,
MessageBarType,
ProgressIndicator,
Separator,
Stack,
TeachingBubble,
Text,
TextField,
} from "@fluentui/react";
import { FeedbackLabels, HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import QueryError, { QueryErrorSeverity } from "Common/QueryError";
import { createUri } from "Common/UrlUtility";
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
import {
SuggestedPrompt,
getSampleDatabaseSuggestedPrompts,
getSuggestedPrompts,
readPromptHistory,
savePromptHistory,
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React, { useMemo, useRef, useState } from "react";
import HintIcon from "../../../images/Hint.svg";
import RecentIcon from "../../../images/Recent.svg";
import errorIcon from "../../../images/close-black.svg";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useTabs } from "../../hooks/useTabs";
import { useCopilotStore } from "../QueryCopilot/QueryCopilotContext";
import { useSelectedNode } from "../useSelectedNode";
type QueryCopilotPromptProps = QueryCopilotProps & {
databaseId: string;
containerId: string;
toggleCopilot: (toggle: boolean) => void;
};
const promptStyles: IButtonStyles = {
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
label: {
fontWeight: 400,
textAlign: "left",
paddingLeft: 8,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
},
textContainer: { overflow: "hidden" },
};
export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
explorer,
toggleCopilot,
databaseId,
containerId,
}: QueryCopilotPromptProps): JSX.Element => {
const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState<boolean>(false);
const inputEdited = useRef(false);
const itemRefs = useRef([]);
const searchInputRef = useRef(null);
const copyQueryRef = useRef(null);
const {
openFeedbackModal,
hideFeedbackModalForLikedQueries,
userPrompt,
setUserPrompt,
generatedQuery,
setGeneratedQuery,
query,
setQuery,
isGeneratingQuery,
setIsGeneratingQuery,
likeQuery,
setLikeQuery,
dislikeQuery,
setDislikeQuery,
showCallout,
setShowCallout,
isSamplePromptsOpen,
setIsSamplePromptsOpen,
showSamplePrompts,
setShowSamplePrompts,
showPromptTeachingBubble,
setShowPromptTeachingBubble,
showDeletePopup,
setShowDeletePopup,
showFeedbackBar,
setShowFeedbackBar,
showCopyPopup,
setshowCopyPopup,
showErrorMessageBar,
showInvalidQueryMessageBar,
setShowInvalidQueryMessageBar,
setShowErrorMessageBar,
setGeneratedQueryComments,
setQueryResults,
setErrors,
errors,
} = useCopilotStore();
const [focusedIndex, setFocusedIndex] = useState(-1);
const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen,
setIsSamplePromptsOpen: setIsSamplePromptsOpen,
setTextBox: setUserPrompt,
};
const copyGeneratedCode = () => {
if (!query) {
return;
}
const queryElement = document.createElement("textarea");
queryElement.value = query;
document.body.appendChild(queryElement);
queryElement.select();
document.execCommand("copy");
document.body.removeChild(queryElement);
setshowCopyPopup(true);
copyQueryRef.current.focus();
setTimeout(() => {
setshowCopyPopup(false);
}, 6000);
};
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
const [histories, setHistories] = useState<string[]>(() => readPromptHistory(userContext.databaseAccount));
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
? getSampleDatabaseSuggestedPrompts()
: getSuggestedPrompts();
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
const { UpArrow, DownArrow, Enter } = NormalizedEventKey;
const handleUserPromptChange = (event: React.ChangeEvent<HTMLInputElement>) => {
inputEdited.current = true;
const { value } = event.target;
setUserPrompt(value);
// Filter history prompts
const filteredHistory = histories.filter((history) => history.toLowerCase().includes(value.toLowerCase()));
setFilteredHistories(filteredHistory);
// Filter suggested prompts
const filteredSuggested = suggestedPrompts.filter((prompt) =>
prompt.text.toLowerCase().includes(value.toLowerCase()),
);
setFilteredSuggestedPrompts(filteredSuggested);
};
const updateHistories = (): void => {
const formattedUserPrompt = userPrompt.replace(/\s+/g, " ").trim();
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
const updatedHistories = existingHistories.filter(
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase(),
);
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
setHistories(newHistories);
savePromptHistory(userContext.databaseAccount, newHistories);
};
const resetMessageStates = (): void => {
setShowErrorMessageBar(false);
setShowInvalidQueryMessageBar(false);
setShowFeedbackBar(false);
};
const resetQueryResults = (): void => {
setQueryResults(null);
setErrors([]);
};
const generateSQLQuery = async (): Promise<void> => {
try {
resetMessageStates();
setIsGeneratingQuery(true);
setShowDeletePopup(false);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
const mode: string = isSampleCopilotActive ? "Sample" : "User";
await allocatePhoenixContainer({ explorer, databaseId, containerId, mode });
const payload = {
userPrompt: userPrompt,
};
useQueryCopilot.getState().refreshCorrelationId();
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
const queryUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery")
: createUri(serverInfo.notebookServerEndpoint, "public/generateSQLQuery");
const response = await fetch(queryUri, {
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`,
},
body: JSON.stringify(payload),
});
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
if (response.ok) {
if (generateSQLQueryResponse?.sql !== "N/A") {
const queryExplanation = `-- **Explanation of query:** ${
generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A"
}\r\n`;
const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql;
const lastQuery = generatedQuery && query ? `${query}\r\n` : "";
setQuery(`${lastQuery}${currentGeneratedQuery}`);
setGeneratedQuery(generateSQLQueryResponse.sql);
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
setShowFeedbackBar(true);
resetQueryResults();
TelemetryProcessor.traceSuccess(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
copilotLatency:
Date.parse(generateSQLQueryResponse?.generateEnd) - Date.parse(generateSQLQueryResponse?.generateStart),
responseCode: response.status,
});
} else {
setShowInvalidQueryMessageBar(true);
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
responseCode: response.status,
});
}
} else if (response?.status === HttpStatusCodes.TooManyRequests) {
handleError(JSON.stringify(generateSQLQueryResponse), "copilotTooManyRequestError");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
setErrors([
new QueryError(
"Ratelimit exceeded 5 per 1 minute. Please try again after sometime",
QueryErrorSeverity.Error,
),
]);
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
responseCode: response.status,
});
} else {
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
responseCode: response.status,
});
}
} catch (error) {
handleError(error, "executeNaturalLanguageQuery");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
throw error;
} finally {
setIsGeneratingQuery(false);
useTabs.getState().setIsTabExecuting(false);
}
};
const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => {
setCopilotTeachingBubbleVisible(visible);
setShowPromptTeachingBubble(visible);
};
const clearFeedback = () => {
resetButtonState();
resetQueryResults();
};
const resetButtonState = () => {
setDislikeQuery(false);
setLikeQuery(false);
setShowCallout(false);
};
const startGenerateQueryProcess = () => {
updateHistories();
generateSQLQuery();
resetButtonState();
};
const getAriaLabel = () => {
if (isGeneratingQuery === null) {
return " ";
} else if (isGeneratingQuery) {
return "Thinking";
} else {
return "Content is updated";
}
};
const openSamplePrompts = () => {
inputEdited.current = true;
setShowSamplePrompts(true);
};
const totalSuggestions = useMemo(
() => [...filteredSuggestedPrompts, ...filteredHistories],
[filteredSuggestedPrompts, filteredHistories],
);
const handleKeyDownForInput = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === DownArrow) {
setFocusedIndex(0);
itemRefs.current[0]?.current?.focus();
} else if (event.key === Enter && userPrompt) {
inputEdited.current = true;
startGenerateQueryProcess();
}
};
const handleKeyDownForItem = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === UpArrow && focusedIndex > 0) {
itemRefs.current[focusedIndex - 1].current?.focus();
setFocusedIndex((prevIndex) => prevIndex - 1);
} else if (event.key === DownArrow && focusedIndex < totalSuggestions.length - 1) {
itemRefs.current[focusedIndex + 1].current?.focus();
setFocusedIndex((prevIndex) => prevIndex + 1);
}
};
React.useEffect(() => {
itemRefs.current = totalSuggestions.map(() => React.createRef());
}, [totalSuggestions]);
React.useEffect(() => {
useTabs.getState().setIsQueryErrorThrown(false);
}, []);
return (
<Stack
className="copilot-prompt-pane"
styles={{ root: { backgroundColor: "#FAFAFA", padding: "8px" } }}
id="copilot-textfield-label"
>
<Stack
horizontal
styles={{
root: {
width: "100%",
borderWidth: 1,
borderStyle: "solid",
borderColor: "#D1D1D1",
borderRadius: 8,
boxShadow: "0px 4px 8px 0px #00000024",
},
}}
>
<Stack style={{ width: "100%" }}>
<Stack horizontal verticalAlign="center" style={{ padding: "8px 8px 0px 8px" }}>
<TextField
id="naturalLanguageInput"
value={userPrompt}
onChange={handleUserPromptChange}
onClick={openSamplePrompts}
onFocus={() => setShowSamplePrompts(true)}
elementRef={searchInputRef}
onKeyDown={handleKeyDownForInput}
style={{ lineHeight: 30 }}
styles={{
root: { width: "100%" },
suffix: { background: "none", padding: 0 },
fieldGroup: {
borderRadius: 4,
borderColor: "#D1D1D1",
"::after": {
border: "inherit",
borderWidth: 2,
borderBottomColor: "#464FEB",
borderRadius: 4,
},
},
}}
disabled={isGeneratingQuery}
autoComplete="off"
placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label"
onRenderSuffix={() => {
return (
<IconButton
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery || !userPrompt.trim()}
allowDisabledFocus={true}
style={{ background: "none" }}
onClick={() => startGenerateQueryProcess()}
aria-label="Send"
/>
);
}}
/>
{showPromptTeachingBubble && copilotTeachingBubbleVisible && (
<TeachingBubble
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
target="#naturalLanguageInput"
hasCloseButton={true}
closeButtonAriaLabel="Close"
onDismiss={() => toggleCopilotTeachingBubbleVisible(false)}
hasSmallHeadline={true}
headline="Write a prompt"
>
Write a prompt here and Query Advisor will generate the query for you. You can also choose from our{" "}
<Link
onClick={() => {
setShowSamplePrompts(true);
toggleCopilotTeachingBubbleVisible(false);
}}
style={{ color: "white", fontWeight: 600 }}
>
sample prompts
</Link>{" "}
or write your own query
</TeachingBubble>
)}
{showSamplePrompts && (
<Callout
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
target="#naturalLanguageInput"
isBeakVisible={false}
onDismiss={() => setShowSamplePrompts(false)}
directionalHintFixed={true}
directionalHint={DirectionalHint.bottomLeftEdge}
alignTargetEdge={true}
gapSpace={4}
>
<Stack>
{filteredHistories?.length > 0 && (
<Stack>
<Text
style={{
width: "100%",
fontSize: 14,
fontWeight: 600,
color: "#0078D4",
marginLeft: 16,
padding: "4px 0",
}}
>
Recent
</Text>
{filteredHistories.map((history, i) => (
<DefaultButton
key={i}
onClick={() => {
setUserPrompt(history);
setShowSamplePrompts(false);
inputEdited.current = true;
}}
elementRef={itemRefs.current[i]}
onKeyDown={handleKeyDownForItem}
onRenderIcon={() => <Image src={RecentIcon} styles={{ root: { overflow: "unset" } }} />}
styles={promptStyles}
>
{history}
</DefaultButton>
))}
</Stack>
)}
{filteredSuggestedPrompts?.length > 0 && (
<Stack>
<Text
style={{
width: "100%",
fontSize: 14,
fontWeight: 600,
color: "#0078D4",
marginLeft: 16,
padding: "4px 0",
}}
>
Suggested Prompts
</Text>
{filteredSuggestedPrompts.map((prompt, index) => (
<DefaultButton
key={prompt.id}
elementRef={itemRefs.current[filteredHistories.length + index]}
onClick={() => {
setUserPrompt(prompt.text);
setShowSamplePrompts(false);
inputEdited.current = true;
}}
onKeyDown={handleKeyDownForItem}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
{prompt.text}
</DefaultButton>
))}
</Stack>
)}
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
<Stack>
<Separator
styles={{
root: {
selectors: { "::before": { background: "#E1DFDD" } },
padding: 0,
},
}}
/>
<Text
style={{
width: "100%",
fontSize: 14,
marginLeft: 16,
padding: "4px 0",
}}
>
Learn about{" "}
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
writing effective prompts
</Link>
</Text>
</Stack>
)}
</Stack>
</Callout>
)}
</Stack>
{!isGeneratingQuery && (
<Stack style={{ padding: 8 }}>
{!showFeedbackBar && (
<Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link
href="https://aka.ms/cdb-copilot-preview-terms"
target="_blank"
style={{ color: "#0072D4" }}
className="underlinedLink"
>
Read preview terms
</Link>
{showErrorMessageBar && (
<MessageBar messageBarType={MessageBarType.error}>
{errors.length > 0
? errors[0].message
: "We ran into an error and were not able to execute query."}
</MessageBar>
)}
{showInvalidQueryMessageBar && (
<MessageBar
messageBarType={MessageBarType.info}
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
>
We were unable to generate a query based upon the prompt provided. Please modify the prompt and
try again. For examples of how to write a good prompt, please read
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
this article.
</Link>{" "}
Our content guidelines can be found
<Link href="https://aka.ms/cdb-query-copilot" target="_blank">
here.
</Link>
</MessageBar>
)}
</Text>
)}
{showFeedbackBar && (
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
{userContext.feedbackPolicies?.policyAllowFeedback && (
<Stack horizontal verticalAlign="center">
<Text style={{ fontSize: 12 }}>{FeedbackLabels.provideFeedback}</Text>
{showCallout && !hideFeedbackModalForLikedQueries && (
<Callout
role="status"
style={{ padding: "6px 12px" }}
styles={{
root: {
borderRadius: 8,
},
beakCurtain: {
borderRadius: 8,
},
calloutMain: {
borderRadius: 8,
},
}}
target="#likeBtn"
onDismiss={() => {
setShowCallout(false);
SubmitFeedback({
params: {
generatedQuery: generatedQuery,
likeQuery: likeQuery,
description: "",
userPrompt: userPrompt,
},
explorer,
databaseId,
containerId,
mode: isSampleCopilotActive ? "Sample" : "User",
});
}}
directionalHint={DirectionalHint.topCenter}
>
<Text>
Thank you. Need to give{" "}
<Link
onClick={() => {
setShowCallout(false);
openFeedbackModal(generatedQuery, true, userPrompt);
}}
>
more feedback?
</Link>
</Text>
</Callout>
)}
<IconButton
id="likeBtn"
style={{ marginLeft: 10 }}
aria-label={FeedbackLabels.provideFeedback}
role="button"
title="Like"
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => {
setShowCallout(!likeQuery);
setLikeQuery(!likeQuery);
if (likeQuery === true) {
document.getElementById("likeStatus").innerHTML = "Unpressed";
}
if (likeQuery === false) {
document.getElementById("likeStatus").innerHTML = "Liked";
}
if (dislikeQuery) {
setDislikeQuery(!dislikeQuery);
}
}}
/>
<IconButton
style={{ margin: "0 4px" }}
role="button"
aria-label={FeedbackLabels.provideFeedback}
title="Dislike"
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => {
let toggleStatusValue = "Unpressed";
if (!dislikeQuery) {
openFeedbackModal(generatedQuery, false, userPrompt);
setLikeQuery(false);
toggleStatusValue = "Disliked";
}
setDislikeQuery(!dislikeQuery);
setShowCallout(false);
document.getElementById("likeStatus").innerHTML = toggleStatusValue;
}}
/>
<span role="status" style={{ position: "absolute", left: "-9999px" }} id="likeStatus"></span>
<Separator
vertical
styles={{
root: {
"::after": {
backgroundColor: "#767676",
},
},
}}
/>
</Stack>
)}
<CommandBarButton
className="copyQuery"
elementRef={copyQueryRef}
onClick={copyGeneratedCode}
iconProps={{ iconName: "Copy" }}
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
styles={{
root: {
backgroundColor: "inherit",
},
}}
>
Copy code
</CommandBarButton>
<CommandBarButton
className="deleteQuery"
onClick={() => {
setShowDeletePopup(true);
}}
iconProps={{ iconName: "Delete" }}
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
styles={{
root: {
backgroundColor: "inherit",
},
}}
>
Clear editor
</CommandBarButton>
</Stack>
)}
</Stack>
)}
{(showFeedbackBar || isGeneratingQuery) && (
<span role="alert" className="screenReaderOnly" aria-label={getAriaLabel()} />
)}
{isGeneratingQuery && (
<ProgressIndicator
label="Thinking..."
ariaLabel={getAriaLabel()}
barHeight={4}
styles={{
root: {
fontSize: 12,
width: "100%",
bottom: 0,
},
itemName: {
padding: "0px 8px",
},
itemProgress: {
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
padding: 0,
},
progressBar: {
backgroundImage:
"linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgb(24, 90, 189) 35%, rgb(71, 207, 250) 70%, rgb(180, 124, 248) 92%, rgba(0, 0, 0, 0))",
animationDuration: "5s",
},
}}
/>
)}
</Stack>
<IconButton
iconProps={{ imageProps: { src: errorIcon } }}
onClick={() => {
toggleCopilot(false);
clearFeedback();
resetMessageStates();
}}
ariaLabel="Close"
title="Close copilot"
/>
</Stack>
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
{query !== "" && query.trim().length !== 0 && (
<DeletePopup
showDeletePopup={showDeletePopup}
setShowDeletePopup={setShowDeletePopup}
setQuery={setQuery}
clearFeedback={clearFeedback}
showFeedbackBar={setShowFeedbackBar}
/>
)}
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
</Stack>
);
};

View File

@@ -1,40 +0,0 @@
import { shallow } from "enzyme";
import { CopilotSubComponentNames } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import React from "react";
import { AppStateComponentNames, StorePath } from "Shared/AppStatePersistenceUtility";
import { updateUserContext } from "UserContext";
import Explorer from "../Explorer";
import { QueryCopilotTab } from "./QueryCopilotTab";
describe("Query copilot tab snapshot test", () => {
it("should render with initial input", () => {
updateUserContext({
databaseAccount: {
name: "name",
properties: undefined,
id: "",
location: "",
type: "",
kind: "",
},
});
const loadState = (path: StorePath) => {
if (
path.componentName === AppStateComponentNames.QueryCopilot &&
path.subComponentName === CopilotSubComponentNames.toggleStatus
) {
return { enabled: true };
} else {
return undefined;
}
};
jest.mock("Shared/AppStatePersistenceUtility", () => ({
loadState,
}));
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,119 +0,0 @@
/* eslint-disable no-console */
import { Stack } from "@fluentui/react";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
import { userContext } from "UserContext";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import React, { useState } from "react";
import SplitterLayout from "react-splitter-layout";
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
readCopilotToggleStatus(userContext.databaseAccount),
);
//TODO: Uncomment this useState when query copilot is reinstated in DE
// const [tabActive, setTabActive] = useState<boolean>(true);
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
const executeQueryBtn = {
iconSrc: ExecuteQueryIcon,
iconAlt: executeQueryBtnLabel,
onCommandClick: () => OnExecuteQueryClick(useQueryCopilot as Partial<QueryCopilotState>),
commandButtonLabel: executeQueryBtnLabel,
ariaLabel: executeQueryBtnLabel,
hasPopup: false,
disabled: query?.trim() === "",
};
const saveQueryBtn = {
iconSrc: SaveQueryIcon,
iconAlt: "Save Query",
onCommandClick: () =>
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={explorer} queryToSave={query} />),
commandButtonLabel: "Save Query",
ariaLabel: "Save Query",
hasPopup: false,
disabled: true,
};
const toggleCopilotButton = {
iconSrc: QueryCommandIcon,
iconAlt: "Query Advisor",
onCommandClick: () => {
toggleCopilot(true);
},
commandButtonLabel: "Query Advisor",
ariaLabel: "Query Advisor",
hasPopup: false,
disabled: copilotActive,
};
return [executeQueryBtn, saveQueryBtn, toggleCopilotButton];
};
React.useEffect(() => {
useCommandBar.getState().setContextButtons(getCommandbarButtons());
}, [query, selectedQuery, copilotActive]);
//TODO: Uncomment this effect when query copilot is reinstated in DE
// React.useEffect(() => {
// return () => {
// useTabs.subscribe((state: TabsState) => {
// if (state.activeReactTab === ReactTabKind.QueryCopilot) {
// setTabActive(true);
// } else {
// setTabActive(false);
// }
// });
// };
// }, []);
const toggleCopilot = (toggle: boolean) => {
setCopilotActive(toggle);
saveCopilotToggleStatus(userContext.databaseAccount, toggle);
};
return (
<Stack className="tab-pane" style={{ width: "100%" }}>
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
{/*TODO: Uncomment this section when query copilot is reinstated in DE
{tabActive && copilotActive && (
<QueryCopilotPromptbar
explorer={explorer}
toggleCopilot={toggleCopilot}
databaseId={QueryCopilotSampleDatabaseId}
containerId={QueryCopilotSampleContainerId}
></QueryCopilotPromptbar>
)} */}
<Stack className="tabPaneContentContainer">
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
<EditorReact
language={"sql"}
content={query}
isReadOnly={false}
wordWrap={"on"}
ariaLabel={"Editing Query"}
lineNumbers={"on"}
onContentChanged={(newQuery: string) => setQuery(newQuery)}
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
/>
<QueryCopilotResults />
</SplitterLayout>
</Stack>
</div>
</Stack>
);
};

View File

@@ -1,155 +0,0 @@
import { FeedOptions } from "@azure/cosmos";
import { handleError } from "Common/ErrorHandlingUtils";
import { sampleDataClient } from "Common/SampleDataClient";
import * as commonUtils from "Common/dataAccess/queryDocuments";
import DocumentId from "Explorer/Tree/DocumentId";
import { querySampleDocuments, readSampleDocument } from "./QueryCopilotUtilities";
jest.mock("Explorer/Tree/DocumentId", () => {
return jest.fn().mockImplementation(() => {
return {
id: jest.fn(),
loadDocument: jest.fn(),
};
});
});
jest.mock("Utils/NotificationConsoleUtils", () => ({
logConsoleProgress: jest.fn().mockReturnValue((): void => undefined),
logConsoleError: jest.fn(),
}));
jest.mock("@azure/cosmos", () => ({
FeedOptions: jest.fn(),
QueryIterator: jest.fn(),
Constants: {
HttpHeaders: {},
},
}));
jest.mock("Common/ErrorHandlingUtils", () => ({
handleError: jest.fn(),
}));
jest.mock("Common/dataAccess/queryDocuments", () => ({
getCommonQueryOptions: jest.fn((options) => options),
}));
jest.mock("Common/SampleDataClient");
jest.mock("node-fetch");
jest.mock("Explorer/Explorer", () => {
class MockExplorer {
allocateContainer = jest.fn().mockResolvedValueOnce({});
}
return MockExplorer;
});
jest.mock("hooks/useQueryCopilot", () => {
const mockQueryCopilotStore = {
shouldAllocateContainer: true,
setShouldAllocateContainer: jest.fn(),
correlationId: "mocked-correlation-id",
};
return {
useQueryCopilot: jest.fn(() => mockQueryCopilotStore),
};
});
describe("QueryCopilotUtilities", () => {
beforeEach(() => jest.clearAllMocks());
describe("querySampleDocuments", () => {
(sampleDataClient as jest.Mock).mockReturnValue({
database: jest.fn().mockReturnValue({
container: jest.fn().mockReturnValue({
items: {
query: jest.fn().mockReturnValue([]),
},
}),
}),
});
it("calls getCommonQueryOptions with the provided options", () => {
const query = "sample query";
const options: FeedOptions = { maxItemCount: 10 };
querySampleDocuments(query, options);
expect(commonUtils.getCommonQueryOptions).toHaveBeenCalledWith(options);
});
it("returns the result of items.query method", () => {
const query = "sample query";
const options: FeedOptions = { maxItemCount: 10 };
const mockResult = [
{ id: 1, name: "Document 1" },
{ id: 2, name: "Document 2" },
];
// Mock the items.query method to return the mockResult
(
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").items.query as jest.Mock
).mockReturnValue(mockResult);
const result = querySampleDocuments(query, options);
expect(result).toEqual(mockResult);
});
});
describe("readSampleDocument", () => {
it("should call the read method with the correct parameters", async () => {
(sampleDataClient as jest.Mock).mockReturnValue({
database: jest.fn().mockReturnValue({
container: jest.fn().mockReturnValue({
item: jest.fn().mockReturnValue({
read: jest.fn().mockResolvedValue({
resource: {},
}),
}),
}),
}),
});
const documentId = new DocumentId(null, "DocumentId", []);
const expectedResponse = {};
const result = await readSampleDocument(documentId);
expect(sampleDataClient).toHaveBeenCalled();
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDB");
expect(sampleDataClient().database("CopilotSampleDB").container).toHaveBeenCalledWith("SampleContainer");
expect(
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").item("DocumentId", undefined).read,
).toHaveBeenCalled();
expect(result).toEqual(expectedResponse);
});
it("should handle an error and re-throw it", async () => {
(sampleDataClient as jest.Mock).mockReturnValue({
database: jest.fn().mockReturnValue({
container: jest.fn().mockReturnValue({
item: jest.fn().mockReturnValue({
read: jest.fn().mockRejectedValue(new Error("Mock error")),
}),
}),
}),
});
const errorMock = new Error("Mock error");
const documentId = new DocumentId(null, "DocumentId", []);
await expect(readSampleDocument(documentId)).rejects.toStrictEqual(errorMock);
expect(sampleDataClient).toHaveBeenCalled();
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDB");
expect(sampleDataClient().database("CopilotSampleDB").container).toHaveBeenCalledWith("SampleContainer");
expect(
sampleDataClient().database("CopilotSampleDB").container("SampleContainer").item("DocumentId", undefined).read,
).toHaveBeenCalled();
expect(handleError).toHaveBeenCalledWith(errorMock, "ReadDocument", expect.any(String));
});
});
});

View File

@@ -1,166 +0,0 @@
import { FeedOptions, Item, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { sampleDataClient } from "Common/SampleDataClient";
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
import { DatabaseAccount } from "Contracts/DataModels";
import DocumentId from "Explorer/Tree/DocumentId";
import { AppStateComponentNames, loadState, saveState } from "Shared/AppStatePersistenceUtility";
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
import * as StringUtility from "../../Shared/StringUtility";
export interface SuggestedPrompt {
id: number;
text: string;
}
export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
options = getCommonQueryOptions(options);
return sampleDataClient()
.database(QueryCopilotSampleDatabaseId)
.container(QueryCopilotSampleContainerId)
.items.query(query, options);
};
export const readSampleDocument = async (documentId: DocumentId): Promise<Item> => {
const clearMessage = logConsoleProgress(`Reading item ${documentId.id()}`);
try {
const response = await sampleDataClient()
.database(QueryCopilotSampleDatabaseId)
.container(QueryCopilotSampleContainerId)
.item(documentId.id(), getPartitionKeyValue(documentId))
.read();
return response?.resource;
} catch (error) {
handleError(error, "ReadDocument", `Failed to read item ${documentId.id()}`);
throw error;
} finally {
clearMessage();
}
};
export const getSampleDatabaseSuggestedPrompts = (): SuggestedPrompt[] => {
return [
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
];
};
export const getSuggestedPrompts = (): SuggestedPrompt[] => {
return [
{ id: 1, text: "Show the first 10 items" },
{ id: 2, text: 'Count all the items in my data as "numItems"' },
{ id: 3, text: "Find the oldest item added to my collection" },
];
};
// Prompt history persistence
export enum CopilotSubComponentNames {
promptHistory = "PromptHistory",
toggleStatus = "ToggleStatus",
}
const getLegacyHistoryKey = (databaseAccount: DatabaseAccount): string =>
`${databaseAccount?.id}-queryCopilotHistories`;
const getLegacyToggleStatusKey = (databaseAccount: DatabaseAccount): string =>
`${databaseAccount?.id}-queryCopilotToggleStatus`;
// Migration only needs to run once
let hasMigrated = false;
// Migrate old prompt history to new format
export const migrateCopilotPersistence = (databaseAccount: DatabaseAccount): void => {
if (hasMigrated) {
return;
}
let key = getLegacyHistoryKey(databaseAccount);
let item = localStorage.getItem(key);
if (item !== undefined && item !== null) {
const historyItems = item.split("|");
saveState(
{
componentName: AppStateComponentNames.QueryCopilot,
subComponentName: CopilotSubComponentNames.promptHistory,
globalAccountName: databaseAccount.name,
databaseName: undefined,
containerName: undefined,
},
historyItems,
);
localStorage.removeItem(key);
}
key = getLegacyToggleStatusKey(databaseAccount);
item = localStorage.getItem(key);
if (item !== undefined && item !== null) {
saveState(
{
componentName: AppStateComponentNames.QueryCopilot,
subComponentName: CopilotSubComponentNames.toggleStatus,
globalAccountName: databaseAccount.name,
databaseName: undefined,
containerName: undefined,
},
StringUtility.toBoolean(item),
);
localStorage.removeItem(key);
}
hasMigrated = true;
};
export const readPromptHistory = (databaseAccount: DatabaseAccount): string[] => {
migrateCopilotPersistence(databaseAccount);
return (
(loadState({
componentName: AppStateComponentNames.QueryCopilot,
subComponentName: CopilotSubComponentNames.promptHistory,
globalAccountName: databaseAccount.name,
databaseName: undefined,
containerName: undefined,
}) as string[]) || []
);
};
export const savePromptHistory = (databaseAccount: DatabaseAccount, historyItems: string[]): void => {
saveState(
{
componentName: AppStateComponentNames.QueryCopilot,
subComponentName: CopilotSubComponentNames.promptHistory,
globalAccountName: databaseAccount.name,
databaseName: undefined,
containerName: undefined,
},
historyItems,
);
};
export const readCopilotToggleStatus = (databaseAccount: DatabaseAccount): boolean => {
migrateCopilotPersistence(databaseAccount);
return !!loadState({
componentName: AppStateComponentNames.QueryCopilot,
subComponentName: CopilotSubComponentNames.toggleStatus,
globalAccountName: databaseAccount.name,
databaseName: undefined,
containerName: undefined,
}) as boolean;
};
export const saveCopilotToggleStatus = (databaseAccount: DatabaseAccount, status: boolean): void => {
saveState(
{
componentName: AppStateComponentNames.QueryCopilot,
subComponentName: CopilotSubComponentNames.toggleStatus,
globalAccountName: databaseAccount.name,
databaseName: undefined,
containerName: undefined,
},
status,
);
};

View File

@@ -1,145 +0,0 @@
import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility";
import Explorer from "Explorer/Explorer";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
jest.mock("@azure/cosmos", () => ({
Constants: {
HttpHeaders: {},
},
}));
jest.mock("Common/ErrorHandlingUtils", () => ({
handleError: jest.fn(),
}));
jest.mock("Common/SampleDataClient");
jest.mock("node-fetch");
jest.mock("Explorer/Explorer", () => {
class MockExplorer {
allocateContainer = jest.fn().mockResolvedValueOnce({});
}
return MockExplorer;
});
describe("Query Copilot Client", () => {
beforeEach(() => jest.clearAllMocks());
describe("SubmitFeedback", () => {
const payload = {
like: "like",
generatedSql: "GeneratedQuery",
userPrompt: "UserPrompt",
description: "Description",
contact: "Contact",
};
const mockStore = useQueryCopilot.getState();
mockStore.correlationId = "mocked-correlation-id";
mockStore.notebookServerInfo = {
notebookServerEndpoint: "mocked-endpoint",
authToken: "mocked-token",
forwardingId: "mocked-forwarding-id",
};
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "feedback")
: createUri(useQueryCopilot.getState().notebookServerInfo.notebookServerEndpoint, "public/feedback");
it("should call fetch with the payload with like", async () => {
const mockFetch = jest.fn().mockResolvedValueOnce({});
globalThis.fetch = mockFetch;
await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: {
likeQuery: true,
generatedQuery: "GeneratedQuery",
userPrompt: "UserPrompt",
description: "Description",
contact: "Contact",
},
explorer: new Explorer(),
});
expect(mockFetch).toHaveBeenCalledWith(
feedbackUri,
expect.objectContaining({
headers: expect.objectContaining({
"x-ms-correlationid": "mocked-correlation-id",
}),
}),
);
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(actualBody).toEqual(payload);
});
it("should call fetch with the payload with unlike and empty parameters", async () => {
payload.like = "dislike";
payload.description = "";
payload.contact = "";
const mockFetch = jest.fn().mockResolvedValueOnce({});
globalThis.fetch = mockFetch;
await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: {
likeQuery: false,
generatedQuery: "GeneratedQuery",
userPrompt: "UserPrompt",
description: undefined,
contact: undefined,
},
explorer: new Explorer(),
});
expect(mockFetch).toHaveBeenCalledWith(
feedbackUri,
expect.objectContaining({
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-correlationid": "mocked-correlation-id",
Authorization: "token mocked-token",
},
}),
);
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(actualBody).toEqual(payload);
});
it("should handle errors and call handleError", async () => {
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: {
likeQuery: true,
generatedQuery: "GeneratedQuery",
userPrompt: "UserPrompt",
description: "Description",
contact: "Contact",
},
explorer: new Explorer(),
}).catch((error) => {
// eslint-disable-next-line jest/no-conditional-expect
expect(error.message).toEqual("Mock error");
});
expect(handleError).toHaveBeenCalledWith(new Error("Mock error"), expect.any(String));
});
});
});

View File

@@ -1,390 +0,0 @@
import { FeedOptions } from "@azure/cosmos";
import {
Areas,
ConnectionStatusType,
ContainerStatusType,
HttpStatusCodes,
PoolIdType,
QueryCopilotSampleContainerId,
QueryCopilotSampleContainerSchema,
ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "Common/ErrorHandlingUtils";
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import QueryError from "Common/QueryError";
import { createUri } from "Common/UrlUtility";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { configContext } from "ConfigContext";
import {
ContainerConnectionInfo,
CopilotEnabledConfiguration,
FeatureRegistration,
IProvisionData,
} from "Contracts/DataModels";
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
import { useDialog } from "Explorer/Controls/Dialog";
import Explorer from "Explorer/Explorer";
import { querySampleDocuments, readCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs";
async function fetchWithTimeout(
url: string,
headers: {
[x: string]: string;
},
) {
const timeout = 10000;
const options = { timeout };
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await window.fetch(url, {
headers,
...options,
signal: controller.signal,
});
clearTimeout(id);
return response;
}
export const isCopilotFeatureRegistered = async (subscriptionId: string): Promise<boolean> => {
const api_version = "2021-07-01";
const url = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.Features/featureProviders/Microsoft.DocumentDB/subscriptionFeatureRegistrations/MicrosoftCopilotForAzureInCDB?api-version=${api_version}`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
const startKey = traceStart(Action.CheckCopilotFeatureRegistration, {
dataExplorerArea: Areas.Copilot,
});
let response;
try {
response = await fetchWithTimeout(url, headers);
} catch (error) {
traceFailure(Action.CheckCopilotFeatureRegistration, { error: String(error) }, startKey);
return false;
}
if (!response?.ok) {
traceFailure(Action.CheckCopilotFeatureRegistration, { status: response?.status }, startKey);
return false;
}
const featureRegistration = (await response?.json()) as FeatureRegistration;
const registered = featureRegistration?.properties?.state === "Registered";
traceSuccess(Action.CheckCopilotFeatureRegistration, { registered }, startKey);
return registered;
};
export const getCopilotEnabled = async (): Promise<boolean> => {
const backendEndpoint: string = configContext.PORTAL_BACKEND_ENDPOINT;
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
const startKey = traceStart(Action.GetCopilotEnabled, {
dataExplorerArea: Areas.Copilot,
});
let response;
try {
response = await fetchWithTimeout(url, headers);
} catch (error) {
traceFailure(Action.GetCopilotEnabled, { error: String(error) }, startKey);
return false;
}
if (!response?.ok) {
traceFailure(Action.GetCopilotEnabled, { status: response?.status }, startKey);
return false;
}
const copilotPortalConfiguration = (await response?.json()) as CopilotEnabledConfiguration;
const isEnabled = copilotPortalConfiguration?.isEnabled;
traceSuccess(Action.GetCopilotEnabled, { isEnabled }, startKey);
return isEnabled;
};
export const allocatePhoenixContainer = async ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => {
try {
if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot, mode);
} else {
const currentAllocatedSchemaInfo = useQueryCopilot.getState().schemaAllocationInfo;
if (
currentAllocatedSchemaInfo.databaseId !== databaseId ||
currentAllocatedSchemaInfo.containerId !== containerId
) {
await resetPhoenixContainerSchema({ explorer, databaseId, containerId, mode });
}
}
useQueryCopilot.getState().setSchemaAllocationInfo({
databaseId,
containerId,
});
} catch (error) {
traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Copilot,
status: error.status,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
useQueryCopilot.getState().resetContainerConnection();
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
} else {
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
);
}
} finally {
useTabs.getState().setIsTabExecuting(false);
}
};
export const resetPhoenixContainerSchema = async ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => {
try {
const provisionData: IProvisionData = {
poolId: PoolIdType.QueryCopilot,
databaseId: databaseId,
containerId: containerId,
mode: mode,
};
const connectionInfo = await explorer.phoenixClient.allocateContainer(provisionData);
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
await explorer.setNotebookInfo(false, connectionInfo, connectionStatus);
} catch (error) {
traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Copilot,
status: error.status,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
throw error;
}
};
export const SendQueryRequest = async ({
userPrompt,
explorer,
}: {
userPrompt: string;
explorer: Explorer;
}): Promise<void> => {
if (userPrompt.trim() !== "") {
useQueryCopilot
.getState()
.setChatMessages([...useQueryCopilot.getState().chatMessages, { source: 0, message: userPrompt }]);
useQueryCopilot.getState().setIsGeneratingQuery(true);
useQueryCopilot.getState().setShouldIncludeInMessages(true);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
try {
if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot);
}
useQueryCopilot.getState().refreshCorrelationId();
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
const queryUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery")
: createUri(serverInfo.notebookServerEndpoint, "public/generateSQLQuery");
const payload = {
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
userPrompt: userPrompt,
};
const response = await fetch(queryUri, {
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
},
body: JSON.stringify(payload),
});
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
if (response.ok) {
if (generateSQLQueryResponse?.sql) {
const bubbleMessage = `Here is a query which will help you with provided prompt.\r\n **Prompt:** "${userPrompt}"`;
if (useQueryCopilot.getState().shouldIncludeInMessages) {
useQueryCopilot.getState().setChatMessages([
...useQueryCopilot.getState().chatMessages,
{
source: 1,
message: bubbleMessage,
sqlQuery: generateSQLQueryResponse.sql,
explanation: generateSQLQueryResponse.explanation,
},
]);
useQueryCopilot.getState().setShowExplanationBubble(true);
useQueryCopilot.getState().setGeneratedQuery(generateSQLQueryResponse.sql);
useQueryCopilot.getState().setGeneratedQueryComments(generateSQLQueryResponse.explanation);
}
}
} else {
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
useTabs.getState().setIsQueryErrorThrown(true);
}
} catch (error) {
handleError(error, "executeNaturalLanguageQuery");
useTabs.getState().setIsQueryErrorThrown(true);
throw error;
} finally {
useQueryCopilot.getState().setUserPrompt("");
useQueryCopilot.getState().setIsGeneratingQuery(false);
useTabs.getState().setIsTabExecuting(false);
}
}
};
export const SubmitFeedback = async ({
params,
explorer,
databaseId,
containerId,
mode,
}: {
params: FeedbackParams;
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => {
try {
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
const payload = {
like: likeQuery ? "like" : "dislike",
generatedSql: generatedQuery,
userPrompt,
description: description || "",
contact: contact || "",
};
if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway
) {
await allocatePhoenixContainer({ explorer, databaseId, containerId, mode });
}
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "feedback")
: createUri(serverInfo.notebookServerEndpoint, "public/feedback");
await fetch(feedbackUri, {
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`,
},
body: JSON.stringify(payload),
});
} catch (error) {
handleError(error, "copilotSubmitFeedback");
}
};
export const OnExecuteQueryClick = async (useQueryCopilot: Partial<QueryCopilotState>): Promise<void> => {
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
userPrompt: useQueryCopilot.getState().userPrompt,
generatedQuery: useQueryCopilot.getState().generatedQuery,
generatedQueryComments: useQueryCopilot.getState().generatedQueryComments,
executedQuery: useQueryCopilot.getState().selectedQuery || useQueryCopilot.getState().query,
});
const queryToExecute = useQueryCopilot.getState().selectedQuery || useQueryCopilot.getState().query;
const queryIterator = querySampleDocuments(queryToExecute, {
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
} as FeedOptions);
useQueryCopilot.getState().setQueryIterator(queryIterator);
setTimeout(async () => {
await QueryDocumentsPerPage(0, queryIterator, useQueryCopilot);
}, 100);
};
export const QueryDocumentsPerPage = async (
firstItemIndex: number,
queryIterator: MinimalQueryIterator,
useQueryCopilot: Partial<QueryCopilotState>,
): Promise<void> => {
try {
useQueryCopilot.getState().setIsExecuting(true);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
const queryResults: QueryResults = await queryPagesUntilContentPresent(
firstItemIndex,
async (firstItemIndex: number) =>
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex),
);
useQueryCopilot.getState().setQueryResults(queryResults);
useQueryCopilot.getState().setErrors([]);
useQueryCopilot.getState().setShowErrorMessageBar(false);
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
});
} catch (error) {
const isCopilotActive = readCopilotToggleStatus(userContext.databaseAccount);
const errorMessage = getErrorMessage(error);
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
errorMessage,
});
handleError(errorMessage, "executeQueryCopilotTab");
useTabs.getState().setIsQueryErrorThrown(true);
if (isCopilotActive) {
const queryErrors = QueryError.tryParse(error);
useQueryCopilot.getState().setErrors(queryErrors);
useQueryCopilot.getState().setShowErrorMessageBar(true);
}
} finally {
useQueryCopilot.getState().setIsExecuting(false);
useTabs.getState().setIsTabExecuting(false);
}
};

View File

@@ -1,39 +0,0 @@
import Explorer from "Explorer/Explorer";
export interface GenerateSQLQueryResponse {
apiVersion: string;
sql: string;
explanation: string;
generateStart: string;
generateEnd: string;
}
enum MessageSource {
User,
AI,
AIExplanation,
}
export interface CopilotMessage {
source: MessageSource;
message: string;
sqlQuery?: string;
explanation?: string;
}
export interface FeedbackParams {
likeQuery: boolean;
generatedQuery: string;
userPrompt: string;
description?: string;
contact?: string;
}
export interface QueryCopilotProps {
explorer: Explorer;
}
export interface CopilotSchemaAllocationInfo {
databaseId: string;
containerId: string;
}

View File

@@ -1,23 +0,0 @@
import { QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
export const QueryCopilotResults: React.FC = (): JSX.Element => {
return (
<QueryResultSection
isMongoDB={false}
queryEditorContent={useQueryCopilot.getState().selectedQuery || useQueryCopilot.getState().query}
errors={useQueryCopilot.getState().errors}
queryResults={useQueryCopilot.getState().queryResults}
isExecuting={useQueryCopilot.getState().isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage(
firstItemIndex,
useQueryCopilot.getState().queryIterator,
useQueryCopilot as Partial<QueryCopilotState>,
)
}
/>
);
};

View File

@@ -1,91 +0,0 @@
import { DefaultButton, IconButton } from "@fluentui/react";
import { shallow } from "enzyme";
import React from "react";
import { SamplePrompts, SamplePromptsProps } from "./SamplePrompts";
describe("Sample Prompts snapshot test", () => {
const setTextBoxMock = jest.fn();
const setIsSamplePromptsOpenMock = jest.fn();
const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: true,
setIsSamplePromptsOpen: setIsSamplePromptsOpenMock,
setTextBox: setTextBoxMock,
};
beforeEach(() => jest.clearAllMocks());
it("should render properly if isSamplePromptsOpen is true", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
expect(wrapper).toMatchSnapshot();
});
it("should render properly if isSamplePromptsOpen is false", () => {
sampleProps.isSamplePromptsOpen = false;
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
expect(wrapper).toMatchSnapshot();
});
it("should call setTextBox and setIsSamplePromptsOpen(false) when a button is clicked", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
wrapper.find(DefaultButton).at(0).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith("Show me products less than 100 dolars");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
wrapper.find(DefaultButton).at(3).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith(
"Write a query to return all records in this table created in the last thirty days",
);
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
});
it("should call setIsSamplePromptsOpen(false) when the close button is clicked", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
wrapper.find(IconButton).first().simulate("click");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
});
it("should call setTextBox and setIsSamplePromptsOpen(false) when a simple prompt button is clicked", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
wrapper.find(DefaultButton).at(0).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith("Show me products less than 100 dolars");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
wrapper.find(DefaultButton).at(1).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith("Show schema");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
});
it("should call setTextBox and setIsSamplePromptsOpen(false) when an intermediate prompt button is clicked", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
wrapper.find(DefaultButton).at(2).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith(
"Show items with a description that contains a number between 0 and 99 inclusive.",
);
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
wrapper.find(DefaultButton).at(3).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith(
"Write a query to return all records in this table created in the last thirty days",
);
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
});
it("should call setTextBox and setIsSamplePromptsOpen(false) when a complex prompt button is clicked", () => {
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
wrapper.find(DefaultButton).at(4).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith("Show all the products that customer Bob has reviewed.");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
wrapper.find(DefaultButton).at(5).simulate("click");
expect(setTextBoxMock).toHaveBeenCalledWith("Which computers are more than 300 dollars and less than 400 dollars?");
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
});
});

View File

@@ -1,138 +0,0 @@
import { DefaultButton, FontIcon, IconButton, Image, Modal, Stack, Text } from "@fluentui/react";
import React, { Dispatch, SetStateAction } from "react";
import ComplexPrompts from "../../../../../images/ComplexPrompts.svg";
import IntermediatePrompts from "../../../../../images/IntermediatePrompts.svg";
import SimplePrompts from "../../../../../images/SimplePrompts.svg";
export interface SamplePromptsProps {
isSamplePromptsOpen: boolean;
setIsSamplePromptsOpen: Dispatch<SetStateAction<boolean>>;
setTextBox: Dispatch<SetStateAction<string>>;
}
const SampleUserInputs: string[] = [
"Show me products less than 100 dolars",
"Show schema",
"Show items with a description that contains a number between 0 and 99 inclusive.",
"Write a query to return all records in this table created in the last thirty days",
"Show all the products that customer Bob has reviewed.",
"Which computers are more than 300 dollars and less than 400 dollars?",
];
export const SamplePrompts = ({ sampleProps }: { sampleProps: SamplePromptsProps }): JSX.Element => {
const updateTextBox = (userInput: string) => {
sampleProps.setTextBox(userInput);
sampleProps.setIsSamplePromptsOpen(false);
};
return (
<Modal isOpen={sampleProps.isSamplePromptsOpen}>
<Stack
style={{ padding: "16px 24px", overflowY: "auto", maxHeight: "calc(100vh - 120px)" }}
role="dialog"
aria-modal="true"
>
<Stack>
<Stack horizontal style={{ display: "flex", justifyContent: "space-between" }}>
<Text style={{ fontSize: 24, fontWeight: 600 }}>Sample Prompts</Text>
<IconButton
styles={{
root: {
border: "none",
backgroundColor: "transparent",
padding: 0,
selectors: {
"&:hover": {
backgroundColor: "transparent",
color: "#000", // Set the desired color for the X button on hover
},
"&:focus": {
outline: "none",
},
},
},
}}
iconProps={{ iconName: "Cancel" }}
onClick={() => sampleProps.setIsSamplePromptsOpen(false)}
/>
</Stack>
<Text style={{ fontWeight: 400, fontSize: 13, marginTop: 10 }}>
Here are some sample prompts for writing queries in NoSQL, ranging from simple to complex
</Text>
</Stack>
<Stack style={{ marginTop: 30, display: "flex" }}>
<Stack horizontal verticalAlign="center">
<Image style={{ width: 25, height: 25 }} src={SimplePrompts} />
<Text style={{ fontSize: 14, fontWeight: 600 }}>Simple Prompts</Text>
</Stack>
</Stack>
<Stack horizontal style={{ gap: 35 }}>
<DefaultButton
style={{ width: 352, height: 135, background: "#F6F6F7" }}
onClick={() => updateTextBox(SampleUserInputs[0])}
>
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[0]}</Text>
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
</DefaultButton>
<DefaultButton
style={{ width: 352, height: 135, background: "#F6F6F7" }}
onClick={() => updateTextBox(SampleUserInputs[1])}
>
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[1]}</Text>
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
</DefaultButton>
</Stack>
<Stack style={{ marginTop: 30, display: "flex" }}>
<Stack horizontal verticalAlign="center">
<Image style={{ width: 25, height: 25 }} src={IntermediatePrompts} />
<Text style={{ fontSize: 14, fontWeight: 600 }}>Intermediate Prompts</Text>
</Stack>
</Stack>
<Stack horizontal style={{ gap: 35 }}>
<DefaultButton
style={{ width: 352, height: 135, background: "#F6F6F7" }}
onClick={() => updateTextBox(SampleUserInputs[2])}
>
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[2]}</Text>
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
</DefaultButton>
<DefaultButton
style={{ width: 352, height: 135, background: "#F6F6F7" }}
onClick={() => updateTextBox(SampleUserInputs[3])}
>
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[3]}</Text>
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
</DefaultButton>
</Stack>
<Stack style={{ marginTop: 30, display: "flex" }}>
<Stack horizontal verticalAlign="center">
<Image style={{ width: 25, height: 25 }} src={ComplexPrompts} />
<Text style={{ fontSize: 14, fontWeight: 600 }}>Complex Prompts</Text>
</Stack>
</Stack>
<Stack horizontal style={{ gap: 35 }}>
<DefaultButton
style={{ width: 352, height: 135, background: "#F6F6F7" }}
onClick={() => updateTextBox(SampleUserInputs[4])}
>
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[4]}</Text>
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
</DefaultButton>
<DefaultButton
style={{ width: 352, height: 135, background: "#F6F6F7" }}
onClick={() => updateTextBox(SampleUserInputs[5])}
>
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[5]}</Text>
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
</DefaultButton>
</Stack>
</Stack>
</Modal>
);
};

View File

@@ -1,781 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Sample Prompts snapshot test should render properly if isSamplePromptsOpen is false 1`] = `
<Modal
isOpen={false}
>
<Stack
aria-modal="true"
role="dialog"
style={
{
"maxHeight": "calc(100vh - 120px)",
"overflowY": "auto",
"padding": "16px 24px",
}
}
>
<Stack>
<Stack
horizontal={true}
style={
{
"display": "flex",
"justifyContent": "space-between",
}
}
>
<Text
style={
{
"fontSize": 24,
"fontWeight": 600,
}
}
>
Sample Prompts
</Text>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
styles={
{
"root": {
"backgroundColor": "transparent",
"border": "none",
"padding": 0,
"selectors": {
"&:focus": {
"outline": "none",
},
"&:hover": {
"backgroundColor": "transparent",
"color": "#000",
},
},
},
}
}
/>
</Stack>
<Text
style={
{
"fontSize": 13,
"fontWeight": 400,
"marginTop": 10,
}
}
>
Here are some sample prompts for writing queries in NoSQL, ranging from simple to complex
</Text>
</Stack>
<Stack
style={
{
"display": "flex",
"marginTop": 30,
}
}
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src={{}}
style={
{
"height": 25,
"width": 25,
}
}
/>
<Text
style={
{
"fontSize": 14,
"fontWeight": 600,
}
}
>
Simple Prompts
</Text>
</Stack>
</Stack>
<Stack
horizontal={true}
style={
{
"gap": 35,
}
}
>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Show me products less than 100 dolars
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Show schema
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
</Stack>
<Stack
style={
{
"display": "flex",
"marginTop": 30,
}
}
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src={{}}
style={
{
"height": 25,
"width": 25,
}
}
/>
<Text
style={
{
"fontSize": 14,
"fontWeight": 600,
}
}
>
Intermediate Prompts
</Text>
</Stack>
</Stack>
<Stack
horizontal={true}
style={
{
"gap": 35,
}
}
>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Show items with a description that contains a number between 0 and 99 inclusive.
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Write a query to return all records in this table created in the last thirty days
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
</Stack>
<Stack
style={
{
"display": "flex",
"marginTop": 30,
}
}
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src={{}}
style={
{
"height": 25,
"width": 25,
}
}
/>
<Text
style={
{
"fontSize": 14,
"fontWeight": 600,
}
}
>
Complex Prompts
</Text>
</Stack>
</Stack>
<Stack
horizontal={true}
style={
{
"gap": 35,
}
}
>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Show all the products that customer Bob has reviewed.
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Which computers are more than 300 dollars and less than 400 dollars?
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
</Stack>
</Stack>
</Modal>
`;
exports[`Sample Prompts snapshot test should render properly if isSamplePromptsOpen is true 1`] = `
<Modal
isOpen={true}
>
<Stack
aria-modal="true"
role="dialog"
style={
{
"maxHeight": "calc(100vh - 120px)",
"overflowY": "auto",
"padding": "16px 24px",
}
}
>
<Stack>
<Stack
horizontal={true}
style={
{
"display": "flex",
"justifyContent": "space-between",
}
}
>
<Text
style={
{
"fontSize": 24,
"fontWeight": 600,
}
}
>
Sample Prompts
</Text>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
styles={
{
"root": {
"backgroundColor": "transparent",
"border": "none",
"padding": 0,
"selectors": {
"&:focus": {
"outline": "none",
},
"&:hover": {
"backgroundColor": "transparent",
"color": "#000",
},
},
},
}
}
/>
</Stack>
<Text
style={
{
"fontSize": 13,
"fontWeight": 400,
"marginTop": 10,
}
}
>
Here are some sample prompts for writing queries in NoSQL, ranging from simple to complex
</Text>
</Stack>
<Stack
style={
{
"display": "flex",
"marginTop": 30,
}
}
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src={{}}
style={
{
"height": 25,
"width": 25,
}
}
/>
<Text
style={
{
"fontSize": 14,
"fontWeight": 600,
}
}
>
Simple Prompts
</Text>
</Stack>
</Stack>
<Stack
horizontal={true}
style={
{
"gap": 35,
}
}
>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Show me products less than 100 dolars
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Show schema
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
</Stack>
<Stack
style={
{
"display": "flex",
"marginTop": 30,
}
}
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src={{}}
style={
{
"height": 25,
"width": 25,
}
}
/>
<Text
style={
{
"fontSize": 14,
"fontWeight": 600,
}
}
>
Intermediate Prompts
</Text>
</Stack>
</Stack>
<Stack
horizontal={true}
style={
{
"gap": 35,
}
}
>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Show items with a description that contains a number between 0 and 99 inclusive.
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Write a query to return all records in this table created in the last thirty days
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
</Stack>
<Stack
style={
{
"display": "flex",
"marginTop": 30,
}
}
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src={{}}
style={
{
"height": 25,
"width": 25,
}
}
/>
<Text
style={
{
"fontSize": 14,
"fontWeight": 600,
}
}
>
Complex Prompts
</Text>
</Stack>
</Stack>
<Stack
horizontal={true}
style={
{
"gap": 35,
}
}
>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Show all the products that customer Bob has reviewed.
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
<CustomizedDefaultButton
onClick={[Function]}
style={
{
"background": "#F6F6F7",
"height": 135,
"width": 352,
}
}
>
<Text
style={
{
"fontSize": 13,
"height": 80,
}
}
>
Which computers are more than 300 dollars and less than 400 dollars?
</Text>
<FontIcon
aria-label="Forward"
iconName="Forward"
style={
{
"left": "92.61%",
"position": "absolute",
}
}
/>
</CustomizedDefaultButton>
</Stack>
</Stack>
</Modal>
`;

View File

@@ -1,69 +0,0 @@
import { Text } from "@fluentui/react";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { ExplanationButton } from "./ExplanationButton";
describe("Explanation Button", () => {
const initialStoreState = useQueryCopilot.getState();
beforeEach(() => {
useQueryCopilot.setState(initialStoreState, true);
useQueryCopilot.getState().showExplanationBubble = true;
useQueryCopilot.getState().shouldIncludeInMessages = false;
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
it("should render explanation bubble with generated comments", () => {
useQueryCopilot.getState().shouldIncludeInMessages = true;
const wrapper = shallow(<ExplanationButton />);
expect(wrapper.find("Stack")).toHaveLength(1);
expect(wrapper.find("Text")).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
});
it("should render 'Explain this query' link", () => {
useQueryCopilot.getState().shouldIncludeInMessages = true;
const mockSetChatMessages = jest.fn();
const mockSetIsGeneratingExplanation = jest.fn();
const mockSetShouldIncludeInMessages = jest.fn();
const mockSetShowExplanationBubble = jest.fn();
useQueryCopilot.getState().setChatMessages = mockSetChatMessages;
useQueryCopilot.getState().setIsGeneratingExplanation = mockSetIsGeneratingExplanation;
useQueryCopilot.getState().setShouldIncludeInMessages = mockSetShouldIncludeInMessages;
useQueryCopilot.getState().setShowExplanationBubble = mockSetShowExplanationBubble;
const wrapper = shallow(<ExplanationButton />);
const textElement = wrapper.find(Text);
textElement.simulate("click");
expect(mockSetChatMessages).toHaveBeenCalledWith([
...initialStoreState.chatMessages,
{ source: 0, message: "Explain this query to me" },
]);
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(true);
expect(mockSetShouldIncludeInMessages).toHaveBeenCalledWith(true);
expect(mockSetShowExplanationBubble).toHaveBeenCalledWith(false);
jest.advanceTimersByTime(3000);
expect(mockSetIsGeneratingExplanation).toHaveBeenCalledWith(false);
expect(mockSetChatMessages).toHaveBeenCalled();
});
it("should render nothing when conditions are not met", () => {
useQueryCopilot.getState().showExplanationBubble = false;
const wrapper = shallow(<ExplanationButton />);
expect(wrapper.isEmptyRender()).toBe(true);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,61 +0,0 @@
import { Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
export const ExplanationButton: React.FC = (): JSX.Element => {
const {
showExplanationBubble,
isGeneratingQuery,
chatMessages,
setChatMessages,
generatedQuery,
generatedQueryComments,
isGeneratingExplanation,
setIsGeneratingExplanation,
setShouldIncludeInMessages,
setShowExplanationBubble,
} = useQueryCopilot();
const showExplanation = () => {
setChatMessages([...chatMessages, { source: 0, message: "Explain this query to me" }]);
setIsGeneratingExplanation(true);
setShouldIncludeInMessages(true);
setShowExplanationBubble(false);
setTimeout(() => {
if (useQueryCopilot.getState().shouldIncludeInMessages) {
setIsGeneratingExplanation(false);
setChatMessages([...chatMessages, { source: 2, message: generatedQueryComments, sqlQuery: generatedQuery }]);
}
}, 3000);
};
return (
showExplanationBubble &&
!isGeneratingQuery &&
!isGeneratingExplanation && (
<Stack
style={{
display: "flex",
alignItems: "center",
padding: "5px 5px 5px 50px",
margin: "5px",
}}
>
<Text
onClick={showExplanation}
style={{
cursor: "pointer",
border: "1.5px solid #B0BEFF",
width: "100%",
padding: "2px",
borderRadius: "4px",
marginBottom: "5px",
}}
>
Explain this query to me
</Text>
</Stack>
)
);
};

View File

@@ -1,32 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Explanation Button should render explanation bubble with generated comments 1`] = `
<Stack
style={
{
"alignItems": "center",
"display": "flex",
"margin": "5px",
"padding": "5px 5px 5px 50px",
}
}
>
<Text
onClick={[Function]}
style={
{
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Explain this query to me
</Text>
</Stack>
`;
exports[`Explanation Button should render nothing when conditions are not met 1`] = `""`;

View File

@@ -1,26 +0,0 @@
import { Stack, Text } from "@fluentui/react";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { FeedbackButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Feedback/FeedbackButtons";
import React from "react";
export const ExplanationBubble = ({ copilotMessage }: { copilotMessage: CopilotMessage }): JSX.Element => {
return (
<Stack
horizontalAlign="start"
verticalAlign="start"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "white",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
<Text>{copilotMessage.message}</Text>
<FeedbackButtons sqlQuery={copilotMessage.sqlQuery} />
<Text style={{ fontWeight: 400, fontSize: "10px", lineHeight: "14px" }}>
AI-generated content may be incorrect
</Text>
</Stack>
);
};

View File

@@ -1,17 +0,0 @@
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { ExplanationBubble } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/ExplainationBubble";
import { shallow } from "enzyme";
import React from "react";
describe("Explanation Bubble snapshot tests", () => {
it("should render", () => {
const mockCopilotMessage: CopilotMessage = {
source: 2,
message: "Mock message",
};
const wrapper = shallow(<ExplanationBubble copilotMessage={mockCopilotMessage} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,38 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Explanation Bubble snapshot tests should render 1`] = `
<Stack
horizontalAlign="start"
style={
{
"backgroundColor": "white",
"borderRadius": "8px",
"margin": "5px 10px",
"textAlign": "start",
}
}
tokens={
{
"childrenGap": 8,
"padding": 8,
}
}
verticalAlign="start"
>
<Text>
Mock message
</Text>
<FeedbackButtons />
<Text
style={
{
"fontSize": "10px",
"fontWeight": 400,
"lineHeight": "14px",
}
}
>
AI-generated content may be incorrect
</Text>
</Stack>
`;

View File

@@ -1,21 +0,0 @@
import { IconButton } from "@fluentui/react";
import { CopyButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Copy/CopyButton";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
document.execCommand = jest.fn();
describe("Copy button snapshot tests", () => {
it("should render and click copy", async () => {
const testInput = "test input query";
useQueryCopilot.getState().setGeneratedQuery(testInput);
const wrapper = shallow(<CopyButton sqlQuery={""} />);
const button = wrapper.find(IconButton).first();
button.simulate("click", {});
expect(document.execCommand).toHaveBeenCalledWith("copy");
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,22 +0,0 @@
import { IconButton } from "@fluentui/react";
import React from "react";
import CopilotCopy from "../../../../../../../../images/CopilotCopy.svg";
export const CopyButton = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
const copyGeneratedCode = (): void => {
const queryElement = document.createElement("textarea");
queryElement.value = sqlQuery;
document.body.appendChild(queryElement);
queryElement.select();
document.execCommand("copy");
document.body.removeChild(queryElement);
};
return (
<IconButton
iconProps={{ imageProps: { src: CopilotCopy } }}
ariaLabel="Copy"
onClick={copyGeneratedCode}
></IconButton>
);
};

View File

@@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Copy button snapshot tests should render and click copy 1`] = `
<CustomizedIconButton
ariaLabel="Copy"
iconProps={
{
"imageProps": {
"src": {},
},
}
}
onClick={[Function]}
/>
`;

View File

@@ -1,205 +0,0 @@
import { Callout, IconButton, Link } from "@fluentui/react";
import { useId } from "@fluentui/react-hooks";
import { FeedbackButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Feedback/FeedbackButtons";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import LikeHover from "../../../../../../../../images/CopilotLikeHover.svg";
import LikePressed from "../../../../../../../../images/CopilotLikePressed.svg";
import LikeRest from "../../../../../../../../images/CopilotLikeRest.svg";
useId as jest.Mock;
jest.mock("../../../../../../../../images/CopilotLikeHover.svg", () => "LikeHover");
jest.mock("../../../../../../../../images/CopilotLikePressed.svg", () => "LikePressed");
jest.mock("../../../../../../../../images/CopilotLikeRest.svg", () => "LikeRest");
beforeEach(() => {
jest.resetAllMocks();
});
describe("Feedback buttons snapshot tests", () => {
it("should click like and show callout", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
const dislikeButton = wrapper.find(IconButton).last();
likeButton.simulate("click");
likeButton = wrapper.find(IconButton).first();
const callout = wrapper.find(Callout).first();
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
expect(callout.exists()).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
it("should click like and dismiss callout", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
const likeButton = wrapper.find(IconButton).first();
likeButton.simulate("click");
let callout = wrapper.find(Callout).first();
callout.simulate("dismiss");
callout = wrapper.find(Callout).first();
expect(callout.exists()).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
it("should click like and submit feedback", () => {
const spy = jest.spyOn(useQueryCopilot.getState(), "openFeedbackModal");
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
const likeButton = wrapper.find(IconButton).first();
likeButton.simulate("click");
const link = wrapper.find(Link).first();
link.simulate("click");
expect(spy).toHaveBeenNthCalledWith(1, "", true, "");
expect(wrapper).toMatchSnapshot();
});
it("should hover over like", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
likeButton.simulate("mouseover");
likeButton = wrapper.find(IconButton).first();
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikeHover);
expect(wrapper).toMatchSnapshot();
});
it("should hover over rest like and leave", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
likeButton.simulate("mouseover");
likeButton.simulate("mouseleave");
likeButton = wrapper.find(IconButton).first();
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
expect(wrapper).toMatchSnapshot();
});
it("should hover over pressed like and leave", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
likeButton.simulate("click");
likeButton = wrapper.find(IconButton).first();
likeButton.simulate("mouseover");
likeButton.simulate("mouseleave");
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
expect(wrapper).toMatchSnapshot();
});
it("should hover over like and click", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
likeButton.simulate("mouseover");
likeButton.simulate("click");
likeButton = wrapper.find(IconButton).first();
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
expect(wrapper).toMatchSnapshot();
});
it("should dobule click on like", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let likeButton = wrapper.find(IconButton).first();
likeButton.simulate("click");
likeButton = wrapper.find(IconButton).first();
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
likeButton.simulate("click");
likeButton = wrapper.find(IconButton).first();
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
expect(wrapper).toMatchSnapshot();
});
it("should click dislike and show popup", () => {
const spy = jest.spyOn(useQueryCopilot.getState(), "openFeedbackModal");
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
const likeButton = wrapper.find(IconButton).first();
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("click");
const callout = wrapper.find(Callout).first();
dislikeButton = wrapper.find(IconButton).last();
expect(likeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
expect(spy).toHaveBeenNthCalledWith(1, "", false, "");
expect(callout.exists()).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
it("should hover over dislike", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("mouseover");
dislikeButton = wrapper.find(IconButton).last();
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikeHover);
expect(wrapper).toMatchSnapshot();
});
it("should hover over rest dislike and leave", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("mouseover");
dislikeButton.simulate("mouseleave");
dislikeButton = wrapper.find(IconButton).last();
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
expect(wrapper).toMatchSnapshot();
});
it("should hover over pressed dislike and leave", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("click");
dislikeButton = wrapper.find(IconButton).last();
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
dislikeButton.simulate("mouseover");
dislikeButton.simulate("mouseleave");
dislikeButton = wrapper.find(IconButton).last();
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
expect(wrapper).toMatchSnapshot();
});
it("should hover over dislike and click", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("mouseover");
dislikeButton.simulate("click");
dislikeButton = wrapper.find(IconButton).last();
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
expect(wrapper).toMatchSnapshot();
});
it("should dobule click on dislike", () => {
const wrapper = shallow(<FeedbackButtons sqlQuery={""} />);
let dislikeButton = wrapper.find(IconButton).last();
dislikeButton.simulate("click");
dislikeButton = wrapper.find(IconButton).last();
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikePressed);
dislikeButton.simulate("click");
dislikeButton = wrapper.find(IconButton).last();
expect(dislikeButton.props().iconProps.imageProps.src).toEqual(LikeRest);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,93 +0,0 @@
import { Callout, DirectionalHint, IconButton, Link, Stack, Text } from "@fluentui/react";
import { useId } from "@fluentui/react-hooks";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React, { useState } from "react";
import LikeHover from "../../../../../../../../images/CopilotLikeHover.svg";
import LikePressed from "../../../../../../../../images/CopilotLikePressed.svg";
import LikeRest from "../../../../../../../../images/CopilotLikeRest.svg";
export const FeedbackButtons = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
const { userPrompt } = useQueryCopilot();
const [likeQuery, setLikeQuery] = useState<boolean>(false);
const [dislikeQuery, setDislikeQuery] = useState<boolean>(false);
const [likeImageLink, setLikeImageLink] = useState<string>(LikeRest);
const [dislikeImageLink, setDislikeImageLink] = useState<string>(LikeRest);
const [calloutVisible, setCalloutVisible] = useState<boolean>(false);
const likeBtnId = useId("likeBtn");
const dislikeBtnId = useId("dislikeBtn");
return (
<Stack horizontal>
{calloutVisible && (
<Callout
target={`#${likeBtnId}`}
onDismiss={() => setCalloutVisible(false)}
directionalHint={DirectionalHint.topCenter}
role="dialog"
style={{ padding: "5px 12px 5px 12px", borderRadius: "4px" }}
styles={{ beakCurtain: { borderRadius: "4px" }, root: { borderRadius: "4px" } }}
>
<Text>
{" "}
<Text>
Thank you. Need to give{" "}
<Link
onClick={() => {
setCalloutVisible(false);
useQueryCopilot.getState().openFeedbackModal(sqlQuery, true, userPrompt);
}}
>
more feedback?
</Link>
</Text>
</Text>
</Callout>
)}
<IconButton
id={likeBtnId}
iconProps={{
imageProps: { src: likeImageLink },
style: { minHeight: "18px" },
}}
onClick={() => {
if (likeQuery) {
setLikeQuery(false);
setLikeImageLink(LikeRest);
setCalloutVisible(false);
} else {
setLikeQuery(true);
setDislikeQuery(false);
setLikeImageLink(LikePressed);
setDislikeImageLink(LikeRest);
setCalloutVisible(true);
}
}}
onMouseOver={() => setLikeImageLink(LikeHover)}
onMouseLeave={() => setLikeImageLink(likeQuery ? LikePressed : LikeRest)}
/>
<IconButton
id={dislikeBtnId}
iconProps={{
imageProps: { src: dislikeImageLink },
style: { minHeight: "18px", transform: "rotate(180deg)" },
}}
onClick={() => {
if (dislikeQuery) {
setDislikeQuery(false);
setDislikeImageLink(LikeRest);
} else {
setDislikeQuery(true);
setLikeQuery(false);
setDislikeImageLink(LikePressed);
setLikeImageLink(LikeRest);
useQueryCopilot.getState().openFeedbackModal(sqlQuery, false, userPrompt);
}
}}
onMouseOver={() => setDislikeImageLink(LikeHover)}
onMouseLeave={() => setDislikeImageLink(dislikeQuery ? LikePressed : LikeRest)}
/>
</Stack>
);
};

View File

@@ -1,666 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Feedback buttons snapshot tests should click dislike and show popup 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn16"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikePressed",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn17"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should click like and dismiss callout 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikePressed",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn2"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn3"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should click like and show callout 1`] = `
<Stack
horizontal={true}
>
<Callout
directionalHint={1}
onDismiss={[Function]}
role="dialog"
style={
{
"borderRadius": "4px",
"padding": "5px 12px 5px 12px",
}
}
styles={
{
"beakCurtain": {
"borderRadius": "4px",
},
"root": {
"borderRadius": "4px",
},
}
}
target="#likeBtn0"
>
<Text>
<Text>
Thank you. Need to give
<StyledLinkBase
onClick={[Function]}
>
more feedback?
</StyledLinkBase>
</Text>
</Text>
</Callout>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikePressed",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn0"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn1"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should click like and submit feedback 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikePressed",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn4"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn5"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should dobule click on dislike 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn26"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn27"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should dobule click on like 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn14"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn15"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should hover over dislike 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn18"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeHover",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn19"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should hover over dislike and click 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn24"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikePressed",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn25"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should hover over like 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeHover",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn6"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn7"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should hover over like and click 1`] = `
<Stack
horizontal={true}
>
<Callout
directionalHint={1}
onDismiss={[Function]}
role="dialog"
style={
{
"borderRadius": "4px",
"padding": "5px 12px 5px 12px",
}
}
styles={
{
"beakCurtain": {
"borderRadius": "4px",
},
"root": {
"borderRadius": "4px",
},
}
}
target="#likeBtn12"
>
<Text>
<Text>
Thank you. Need to give
<StyledLinkBase
onClick={[Function]}
>
more feedback?
</StyledLinkBase>
</Text>
</Text>
</Callout>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikePressed",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn12"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn13"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should hover over pressed dislike and leave 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn22"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikePressed",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn23"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should hover over pressed like and leave 1`] = `
<Stack
horizontal={true}
>
<Callout
directionalHint={1}
onDismiss={[Function]}
role="dialog"
style={
{
"borderRadius": "4px",
"padding": "5px 12px 5px 12px",
}
}
styles={
{
"beakCurtain": {
"borderRadius": "4px",
},
"root": {
"borderRadius": "4px",
},
}
}
target="#likeBtn10"
>
<Text>
<Text>
Thank you. Need to give
<StyledLinkBase
onClick={[Function]}
>
more feedback?
</StyledLinkBase>
</Text>
</Text>
</Callout>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikePressed",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn10"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn11"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should hover over rest dislike and leave 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn20"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn21"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;
exports[`Feedback buttons snapshot tests should hover over rest like and leave 1`] = `
<Stack
horizontal={true}
>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
},
}
}
id="likeBtn8"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
<CustomizedIconButton
iconProps={
{
"imageProps": {
"src": "LikeRest",
},
"style": {
"minHeight": "18px",
"transform": "rotate(180deg)",
},
}
}
id="dislikeBtn9"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
/>
</Stack>
`;

View File

@@ -1,10 +0,0 @@
import { InsertButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Insert/InsertButton";
import { shallow } from "enzyme";
import React from "react";
describe("Insert button snapshot tests", () => {
it("should click and update state", () => {
const wrapper = shallow(<InsertButton sqlQuery={""} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,16 +0,0 @@
import { ActionButton } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import CopilotInsert from "../../../../../../../../images/CopilotInsert.svg";
export const InsertButton = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
return (
<ActionButton
iconProps={{ imageProps: { src: CopilotInsert } }}
style={{ borderRadius: "4px", borderWidth: "1px", borderColor: "#D1D1D1", height: "24px", paddingBottom: "2px" }}
onClick={() => useQueryCopilot.getState().setQuery(sqlQuery)}
>
Insert
</ActionButton>
);
};

View File

@@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Insert button snapshot tests should click and update state 1`] = `
<CustomizedActionButton
iconProps={
{
"imageProps": {
"src": {},
},
}
}
onClick={[Function]}
style={
{
"borderColor": "#D1D1D1",
"borderRadius": "4px",
"borderWidth": "1px",
"height": "24px",
"paddingBottom": "2px",
}
}
>
Insert
</CustomizedActionButton>
`;

View File

@@ -1,11 +0,0 @@
import { MoreButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/More/MoreButton";
import { shallow } from "enzyme";
import React from "react";
describe("More button snapshot tests", () => {
it("should render", () => {
const wrapper = shallow(<MoreButton />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,41 +0,0 @@
import { DirectionalHint, IContextualMenuProps, IconButton } from "@fluentui/react";
import React from "react";
import ExplainIcon from "../../../../../../../../images/CopilotExplain.svg";
import OptimizeIcon from "../../../../../../../../images/CopilotOptimize.svg";
import RegenerateIcon from "../../../../../../../../images/CopilotRegenerate.svg";
import SimplifyIcon from "../../../../../../../../images/CopilotSimplify.svg";
export const MoreButton: React.FC = (): JSX.Element => {
const menuProps: IContextualMenuProps = {
items: [
{
key: "regenerate",
text: "Regenerate code",
iconProps: { imageProps: { src: RegenerateIcon } },
},
{
key: "explain",
text: "Explain code",
iconProps: { imageProps: { src: ExplainIcon } },
},
{
key: "optimize",
text: "Optimize",
iconProps: { imageProps: { src: OptimizeIcon } },
},
{
key: "simplify",
text: "Simplify",
iconProps: { imageProps: { src: SimplifyIcon } },
},
],
directionalHint: DirectionalHint.topRightEdge,
calloutProps: {
styles: { calloutMain: { borderRadius: "4px" }, root: { borderRadius: "4px" } },
},
};
return (
<IconButton iconProps={{ iconName: "More" }} menuProps={menuProps} menuIconProps={{ hidden: true }}></IconButton>
);
};

View File

@@ -1,69 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`More button snapshot tests should render 1`] = `
<CustomizedIconButton
iconProps={
{
"iconName": "More",
}
}
menuIconProps={
{
"hidden": true,
}
}
menuProps={
{
"calloutProps": {
"styles": {
"calloutMain": {
"borderRadius": "4px",
},
"root": {
"borderRadius": "4px",
},
},
},
"directionalHint": 2,
"items": [
{
"iconProps": {
"imageProps": {
"src": {},
},
},
"key": "regenerate",
"text": "Regenerate code",
},
{
"iconProps": {
"imageProps": {
"src": {},
},
},
"key": "explain",
"text": "Explain code",
},
{
"iconProps": {
"imageProps": {
"src": {},
},
},
"key": "optimize",
"text": "Optimize",
},
{
"iconProps": {
"imageProps": {
"src": {},
},
},
"key": "simplify",
"text": "Simplify",
},
],
}
}
/>
`;

View File

@@ -1,11 +0,0 @@
import { OutputBubbleButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/OutputBubbleButtons";
import { shallow } from "enzyme";
import React from "react";
describe("Output Bubble Buttons snapshot tests", () => {
it("should render", () => {
const wrapper = shallow(<OutputBubbleButtons sqlQuery={""} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,25 +0,0 @@
import { Stack } from "@fluentui/react";
import { CopyButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Copy/CopyButton";
import { FeedbackButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Feedback/FeedbackButtons";
import { InsertButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/Insert/InsertButton";
import { MoreButton } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/More/MoreButton";
import React from "react";
export const OutputBubbleButtons = ({ sqlQuery }: { sqlQuery: string }): JSX.Element => {
return (
<Stack horizontal>
<Stack.Item style={{ paddingTop: "5px" }}>
<InsertButton sqlQuery={sqlQuery} />
</Stack.Item>
<Stack.Item>
<CopyButton sqlQuery={sqlQuery} />
</Stack.Item>
<Stack.Item>
<FeedbackButtons sqlQuery={sqlQuery} />
</Stack.Item>
<Stack.Item>
<MoreButton />
</Stack.Item>
</Stack>
);
};

View File

@@ -1,32 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Output Bubble Buttons snapshot tests should render 1`] = `
<Stack
horizontal={true}
>
<StackItem
style={
{
"paddingTop": "5px",
}
}
>
<InsertButton
sqlQuery=""
/>
</StackItem>
<StackItem>
<CopyButton
sqlQuery=""
/>
</StackItem>
<StackItem>
<FeedbackButtons
sqlQuery=""
/>
</StackItem>
<StackItem>
<MoreButton />
</StackItem>
</Stack>
`;

View File

@@ -1,22 +0,0 @@
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
import { shallow } from "enzyme";
import { withHooks } from "jest-react-hooks-shallow";
import React from "react";
describe("Output Bubble snapshot tests", () => {
it("should render and update height", () => {
withHooks(() => {
const wrapper = shallow(
<OutputBubble
copilotMessage={{ message: "testMessage", source: 1, explanation: "testExplanation", sqlQuery: "testSQL" }}
/>,
);
const editor = wrapper.find(EditorReact).first();
expect(editor.props().monacoContainerStyles).not.toHaveProperty("height", undefined);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -1,71 +0,0 @@
import { Stack, Text } from "@fluentui/react";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { OutputBubbleButtons } from "Explorer/QueryCopilot/V2/Bubbles/Output/Buttons/OutputBubbleButtons";
import { userContext } from "UserContext";
import React, { useState } from "react";
export const OutputBubble = ({ copilotMessage }: { copilotMessage: CopilotMessage }): JSX.Element => {
const [windowHeight, setWindowHeight] = useState<string>();
const textHeightWithPadding = 16;
const calculateQueryWindowHeight = (): string => {
const outputWidth = document.getElementById("outputBubble")?.clientWidth;
const responseLength = copilotMessage.sqlQuery.length;
if (outputWidth > responseLength) {
return `${textHeightWithPadding * 3}px`;
} else {
const neededLines = Math.ceil(responseLength / outputWidth);
return `${neededLines * textHeightWithPadding}px`;
}
};
React.useEffect(() => {
if (userContext.features.copilotChatFixedMonacoEditorHeight) {
setWindowHeight(`${textHeightWithPadding * 5}px`);
} else {
setWindowHeight(calculateQueryWindowHeight());
}
}, []);
return (
<Stack
id="outputBubble"
style={{
display: "flex",
alignItems: "center",
padding: "10px",
margin: "10px",
backgroundColor: "white",
borderRadius: "8px",
}}
tokens={{ padding: 8, childrenGap: 8 }}
>
<Stack.Item style={{ alignSelf: "flex-start", paddingLeft: "2px" }}>{copilotMessage.message}</Stack.Item>
<Stack.Item style={{ alignSelf: "stretch", flexGrow: 4 }}>
<EditorReact
language={"sql"}
content={copilotMessage.sqlQuery}
isReadOnly={true}
ariaLabel={"AI Response"}
wordWrap="on"
lineNumbers="on"
lineNumbersMinChars={2}
lineDecorationsWidth={0}
minimap={{ enabled: false }}
scrollBeyondLastLine={false}
monacoContainerStyles={{ height: windowHeight, borderRadius: "4px" }}
/>
</Stack.Item>
<Stack.Item style={{ alignSelf: "flex-start" }}>
<OutputBubbleButtons sqlQuery={copilotMessage.sqlQuery} />
</Stack.Item>
<Stack.Item>
<Text style={{ fontWeight: 400, fontSize: "10px", lineHeight: "14px" }}>
AI-generated content may be incorrect
</Text>
</Stack.Item>
</Stack>
);
};

View File

@@ -1,89 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Output Bubble snapshot tests should render and update height 1`] = `
<Stack
id="outputBubble"
style={
{
"alignItems": "center",
"backgroundColor": "white",
"borderRadius": "8px",
"display": "flex",
"margin": "10px",
"padding": "10px",
}
}
tokens={
{
"childrenGap": 8,
"padding": 8,
}
}
>
<StackItem
style={
{
"alignSelf": "flex-start",
"paddingLeft": "2px",
}
}
>
testMessage
</StackItem>
<StackItem
style={
{
"alignSelf": "stretch",
"flexGrow": 4,
}
}
>
<EditorReact
ariaLabel="AI Response"
content="testSQL"
isReadOnly={true}
language="sql"
lineDecorationsWidth={0}
lineNumbers="on"
lineNumbersMinChars={2}
minimap={
{
"enabled": false,
}
}
monacoContainerStyles={
{
"borderRadius": "4px",
"height": "NaNpx",
}
}
scrollBeyondLastLine={false}
wordWrap="on"
/>
</StackItem>
<StackItem
style={
{
"alignSelf": "flex-start",
}
}
>
<OutputBubbleButtons
sqlQuery="testSQL"
/>
</StackItem>
<StackItem>
<Text
style={
{
"fontSize": "10px",
"fontWeight": 400,
"lineHeight": "14px",
}
}
>
AI-generated content may be incorrect
</Text>
</StackItem>
</Stack>
`;

View File

@@ -1,8 +0,0 @@
@keyframes loadingAnimation {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}

View File

@@ -1,62 +0,0 @@
import { DefaultButton } from "@fluentui/react";
import { shallow } from "enzyme";
import React from "react";
import { RetrievingBubble } from "./RetrievingBubble";
const mockUseQueryCopilot = {
isGeneratingQuery: false,
setIsGeneratingQuery: jest.fn(),
isGeneratingExplanation: false,
setIsGeneratingExplanation: jest.fn(),
shouldIncludeInMessages: true,
setShouldIncludeInMessages: jest.fn(),
};
jest.mock("hooks/useQueryCopilot", () => ({
useQueryCopilot: jest.fn(() => mockUseQueryCopilot),
}));
describe("RetrievingBubble", () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseQueryCopilot.isGeneratingQuery = false;
mockUseQueryCopilot.isGeneratingExplanation = false;
mockUseQueryCopilot.setIsGeneratingQuery.mockClear();
mockUseQueryCopilot.setIsGeneratingExplanation.mockClear();
mockUseQueryCopilot.setShouldIncludeInMessages.mockClear();
});
it("should render properly when isGeneratingQuery is true", () => {
mockUseQueryCopilot.isGeneratingQuery = true;
const wrapper = shallow(<RetrievingBubble />);
expect(wrapper).toMatchSnapshot();
});
it("should render properly when isGeneratingExplanation is true", () => {
mockUseQueryCopilot.isGeneratingExplanation = true;
const wrapper = shallow(<RetrievingBubble />);
expect(wrapper).toMatchSnapshot();
});
it("when isGeneratingQuery is true clicking stop generating button invokes the correct callbacks", () => {
mockUseQueryCopilot.isGeneratingQuery = true;
const wrapper = shallow(<RetrievingBubble />);
wrapper.find(DefaultButton).at(0).simulate("click");
expect(mockUseQueryCopilot.setIsGeneratingQuery).toHaveBeenCalledWith(false);
expect(mockUseQueryCopilot.setIsGeneratingExplanation).toHaveBeenCalledTimes(0);
expect(mockUseQueryCopilot.setShouldIncludeInMessages).toHaveBeenCalledWith(false);
});
it("when isGeneratingExplanation is true clicking stop generating button invokes the correct callbacks", () => {
mockUseQueryCopilot.isGeneratingExplanation = true;
const wrapper = shallow(<RetrievingBubble />);
wrapper.find(DefaultButton).at(0).simulate("click");
expect(mockUseQueryCopilot.setIsGeneratingQuery).toHaveBeenCalledTimes(0);
expect(mockUseQueryCopilot.setIsGeneratingExplanation).toHaveBeenCalledWith(false);
expect(mockUseQueryCopilot.setShouldIncludeInMessages).toHaveBeenCalledWith(false);
});
});

View File

@@ -1,98 +0,0 @@
import { DefaultButton, Image, Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import StopGeneratingIcon from "../../../../../../images/StopGenerating.svg";
import "./RetrievingBubble.css";
export const RetrievingBubble = (): JSX.Element => {
const {
isGeneratingQuery,
setIsGeneratingQuery,
isGeneratingExplanation,
setIsGeneratingExplanation,
shouldIncludeInMessages,
setShouldIncludeInMessages,
} = useQueryCopilot();
const stopGenerating = () => {
if (isGeneratingQuery) {
setIsGeneratingQuery(false);
}
if (isGeneratingExplanation) {
setIsGeneratingExplanation(false);
}
if (shouldIncludeInMessages) {
setShouldIncludeInMessages(false);
}
};
const bubbleContent = (bubbleType: string) => {
return (
<Stack
horizontalAlign="end"
verticalAlign="end"
style={{
display: "flex",
alignItems: "center",
padding: "10px",
margin: "10px",
backgroundColor: "#FAFAFA",
borderRadius: "8px",
}}
>
<Text
style={{
width: "100%",
height: "46px",
backgroundColor: "white",
padding: "12px 16px 16px 16px",
gap: "12px",
borderRadius: "8px",
fontWeight: "bold",
}}
>
Retriveing {bubbleType}
</Text>
<div
style={{
width: "100%",
height: "4px",
backgroundColor: "#E6E6E6",
borderRadius: "4px",
overflow: "hidden",
}}
>
<div
style={{
width: "50%",
height: "100%",
backgroundColor: "#0078D4",
animation: "loadingAnimation 2s linear infinite",
}}
></div>
</div>
<Stack
horizontalAlign="center"
verticalAlign="center"
style={{ marginTop: "8px", gap: "8px", alignItems: "center" }}
>
<DefaultButton
onClick={stopGenerating}
styles={{ root: { border: "none", background: "none", padding: 0, color: "#424242" } }}
style={{ color: "#424242" }}
onRenderIcon={() => <Image src={StopGeneratingIcon} />}
>
Stop generating
</DefaultButton>
</Stack>
</Stack>
);
};
return (
<>
{isGeneratingQuery && bubbleContent("queries")}
{isGeneratingExplanation && bubbleContent("explanation")}
</>
);
};

View File

@@ -1,183 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RetrievingBubble should render properly when isGeneratingExplanation is true 1`] = `
<Fragment>
<Stack
horizontalAlign="end"
style={
{
"alignItems": "center",
"backgroundColor": "#FAFAFA",
"borderRadius": "8px",
"display": "flex",
"margin": "10px",
"padding": "10px",
}
}
verticalAlign="end"
>
<Text
style={
{
"backgroundColor": "white",
"borderRadius": "8px",
"fontWeight": "bold",
"gap": "12px",
"height": "46px",
"padding": "12px 16px 16px 16px",
"width": "100%",
}
}
>
Retriveing
explanation
</Text>
<div
style={
{
"backgroundColor": "#E6E6E6",
"borderRadius": "4px",
"height": "4px",
"overflow": "hidden",
"width": "100%",
}
}
>
<div
style={
{
"animation": "loadingAnimation 2s linear infinite",
"backgroundColor": "#0078D4",
"height": "100%",
"width": "50%",
}
}
/>
</div>
<Stack
horizontalAlign="center"
style={
{
"alignItems": "center",
"gap": "8px",
"marginTop": "8px",
}
}
verticalAlign="center"
>
<CustomizedDefaultButton
onClick={[Function]}
onRenderIcon={[Function]}
style={
{
"color": "#424242",
}
}
styles={
{
"root": {
"background": "none",
"border": "none",
"color": "#424242",
"padding": 0,
},
}
}
>
Stop generating
</CustomizedDefaultButton>
</Stack>
</Stack>
</Fragment>
`;
exports[`RetrievingBubble should render properly when isGeneratingQuery is true 1`] = `
<Fragment>
<Stack
horizontalAlign="end"
style={
{
"alignItems": "center",
"backgroundColor": "#FAFAFA",
"borderRadius": "8px",
"display": "flex",
"margin": "10px",
"padding": "10px",
}
}
verticalAlign="end"
>
<Text
style={
{
"backgroundColor": "white",
"borderRadius": "8px",
"fontWeight": "bold",
"gap": "12px",
"height": "46px",
"padding": "12px 16px 16px 16px",
"width": "100%",
}
}
>
Retriveing
queries
</Text>
<div
style={
{
"backgroundColor": "#E6E6E6",
"borderRadius": "4px",
"height": "4px",
"overflow": "hidden",
"width": "100%",
}
}
>
<div
style={
{
"animation": "loadingAnimation 2s linear infinite",
"backgroundColor": "#0078D4",
"height": "100%",
"width": "50%",
}
}
/>
</div>
<Stack
horizontalAlign="center"
style={
{
"alignItems": "center",
"gap": "8px",
"marginTop": "8px",
}
}
verticalAlign="center"
>
<CustomizedDefaultButton
onClick={[Function]}
onRenderIcon={[Function]}
style={
{
"color": "#424242",
}
}
styles={
{
"root": {
"background": "none",
"border": "none",
"color": "#424242",
"padding": 0,
},
}
}
>
Stop generating
</CustomizedDefaultButton>
</Stack>
</Stack>
</Fragment>
`;

View File

@@ -1,26 +0,0 @@
import { Text } from "@fluentui/react";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { SampleBubble } from "./SampleBubble";
describe("Sample Bubble snapshot test", () => {
it("should render", () => {
const wrapper = shallow(<SampleBubble />);
const sampleInputs = wrapper.find(Text);
expect(sampleInputs.length).toEqual(2);
expect(useQueryCopilot.getState().userPrompt).toEqual("");
expect(wrapper).toMatchSnapshot();
});
it("should render and be clicked", () => {
const wrapper = shallow(<SampleBubble />);
const firstSampleInput = wrapper.find(Text).first();
firstSampleInput.simulate("click", {}, "");
expect(useQueryCopilot.getState().userPrompt).toEqual(expect.any(String));
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,42 +0,0 @@
import { Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
export const SampleBubble: React.FC = (): JSX.Element => {
const { setUserPrompt } = useQueryCopilot();
const sampleChatMessages: string[] = [
"Write a query to return last 10 records in the database",
'Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"',
];
return (
<Stack
horizontalAlign="end"
verticalAlign="end"
style={{
display: "flex",
alignItems: "center",
padding: "10px",
margin: "10px",
}}
>
{sampleChatMessages.map((text, index) => (
<Text
key={index}
onClick={() => setUserPrompt(text)}
style={{
cursor: "pointer",
border: "1.5px solid #B0BEFF",
width: "100%",
padding: "2px",
borderRadius: "4px",
marginBottom: "5px",
}}
>
{text}
</Text>
))}
</Stack>
);
};

View File

@@ -1,97 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Sample Bubble snapshot test should render 1`] = `
<Stack
horizontalAlign="end"
style={
{
"alignItems": "center",
"display": "flex",
"margin": "10px",
"padding": "10px",
}
}
verticalAlign="end"
>
<Text
key="0"
onClick={[Function]}
style={
{
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Write a query to return last 10 records in the database
</Text>
<Text
key="1"
onClick={[Function]}
style={
{
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"
</Text>
</Stack>
`;
exports[`Sample Bubble snapshot test should render and be clicked 1`] = `
<Stack
horizontalAlign="end"
style={
{
"alignItems": "center",
"display": "flex",
"margin": "10px",
"padding": "10px",
}
}
verticalAlign="end"
>
<Text
key="0"
onClick={[Function]}
style={
{
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Write a query to return last 10 records in the database
</Text>
<Text
key="1"
onClick={[Function]}
style={
{
"border": "1.5px solid #B0BEFF",
"borderRadius": "4px",
"cursor": "pointer",
"marginBottom": "5px",
"padding": "2px",
"width": "100%",
}
}
>
Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"
</Text>
</Stack>
`;

View File

@@ -1,13 +0,0 @@
import { shallow } from "enzyme";
import React from "react";
import { WelcomeBubble } from "./WelcomeBubble";
const mockedDate = new Date(2023, 7, 15);
jest.spyOn(global, "Date").mockImplementation(() => mockedDate);
describe("Welcome Bubble snapshot test", () => {
it("should render", () => {
const wrapper = shallow(<WelcomeBubble />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,52 +0,0 @@
import { Stack, Text } from "@fluentui/react";
import React from "react";
export const WelcomeBubble: React.FC = (): JSX.Element => {
return (
<Stack>
<Stack horizontalAlign="center" style={{ color: "#707070" }} tokens={{ padding: 8, childrenGap: 8 }}>
{new Date().toLocaleDateString("en-US", {
month: "long",
day: "numeric",
})}{" "}
{new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "numeric",
hour12: true,
})}
</Stack>
<Stack
tokens={{ padding: 16, childrenGap: 12 }}
style={{
backgroundColor: "white",
margin: "5px 10px",
borderRadius: "8px",
}}
>
<Text variant="medium">
Hello, I am Cosmos Db&apos;s copilot assistant. I can help you do the following things:
</Text>
<Stack tokens={{ childrenGap: 8 }}>
<Stack horizontal style={{ marginLeft: "15px" }} tokens={{ childrenGap: 8 }}>
<Text style={{ fontSize: "16px", lineHeight: "16px", verticalAlign: "middle" }}></Text>
<Text style={{ verticalAlign: "middle" }}>Generate queries based upon prompt you suggest</Text>
</Stack>
<Stack horizontal style={{ marginLeft: "15px" }} tokens={{ childrenGap: 8 }}>
<Text style={{ fontSize: "16px", lineHeight: "16px", verticalAlign: "middle" }}></Text>
<Text style={{ verticalAlign: "middle" }}>
Explain and provide alternate queries for a query suggested by you
</Text>
</Stack>
<Stack horizontal style={{ marginLeft: "15px" }} tokens={{ childrenGap: 8 }}>
<Text style={{ fontSize: "16px", lineHeight: "16px", verticalAlign: "middle" }}></Text>
<Text style={{ verticalAlign: "middle" }}>Help answer questions about Cosmos DB</Text>
</Stack>
</Stack>
<Text variant="medium">
To get started, ask me a question or use one of the sample prompts to generate a query. AI-generated content
may be incorrect.
</Text>
</Stack>
</Stack>
);
};

View File

@@ -1,160 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Welcome Bubble snapshot test should render 1`] = `
<Stack>
<Stack
horizontalAlign="center"
style={
{
"color": "#707070",
}
}
tokens={
{
"childrenGap": 8,
"padding": 8,
}
}
>
August 15
12:00 AM
</Stack>
<Stack
style={
{
"backgroundColor": "white",
"borderRadius": "8px",
"margin": "5px 10px",
}
}
tokens={
{
"childrenGap": 12,
"padding": 16,
}
}
>
<Text
variant="medium"
>
Hello, I am Cosmos Db's copilot assistant. I can help you do the following things:
</Text>
<Stack
tokens={
{
"childrenGap": 8,
}
}
>
<Stack
horizontal={true}
style={
{
"marginLeft": "15px",
}
}
tokens={
{
"childrenGap": 8,
}
}
>
<Text
style={
{
"fontSize": "16px",
"lineHeight": "16px",
"verticalAlign": "middle",
}
}
>
</Text>
<Text
style={
{
"verticalAlign": "middle",
}
}
>
Generate queries based upon prompt you suggest
</Text>
</Stack>
<Stack
horizontal={true}
style={
{
"marginLeft": "15px",
}
}
tokens={
{
"childrenGap": 8,
}
}
>
<Text
style={
{
"fontSize": "16px",
"lineHeight": "16px",
"verticalAlign": "middle",
}
}
>
</Text>
<Text
style={
{
"verticalAlign": "middle",
}
}
>
Explain and provide alternate queries for a query suggested by you
</Text>
</Stack>
<Stack
horizontal={true}
style={
{
"marginLeft": "15px",
}
}
tokens={
{
"childrenGap": 8,
}
}
>
<Text
style={
{
"fontSize": "16px",
"lineHeight": "16px",
"verticalAlign": "middle",
}
}
>
</Text>
<Text
style={
{
"verticalAlign": "middle",
}
}
>
Help answer questions about Cosmos DB
</Text>
</Stack>
</Stack>
<Text
variant="medium"
>
To get started, ask me a question or use one of the sample prompts to generate a query. AI-generated content may be incorrect.
</Text>
</Stack>
</Stack>
`;

View File

@@ -1,112 +0,0 @@
import { IconButton, Image, TextField } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { SendQueryRequest } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { act } from "react-dom/test-utils";
import { Footer } from "./Footer";
jest.mock("@azure/cosmos", () => ({
Constants: {
HttpHeaders: {},
},
}));
jest.mock("Common/SampleDataClient");
jest.mock("Explorer/Explorer");
jest.mock("node-fetch");
jest.mock("Common/ErrorHandlingUtils", () => ({
handleError: jest.fn(),
getErrorMessage: jest.fn(),
}));
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
describe("Footer snapshot test", () => {
const testMessage = "test message";
const initialStoreState = useQueryCopilot.getState();
beforeEach(() => {
useQueryCopilot.setState(initialStoreState, true);
});
it("should open sample prompts on button click", () => {
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const samplePromptsImage = wrapper.find(Image).first();
samplePromptsImage.simulate("click", {});
expect(useQueryCopilot.getState().isSamplePromptsOpen).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
it("should update user input", () => {
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const newInput = "some new input";
const textInput = wrapper.find(TextField).first();
textInput.simulate("change", {}, newInput);
expect(useQueryCopilot.getState().userPrompt).toEqual(newInput);
expect(wrapper).toMatchSnapshot();
});
it("should pass text with enter key", async () => {
useQueryCopilot.getState().setUserPrompt(testMessage);
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const textInput = wrapper.find(TextField).first();
await act(async () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
textInput.simulate("keydown", { key: "Enter", shiftKey: false, preventDefault: () => {} });
});
await Promise.resolve();
expect(SendQueryRequest).toHaveBeenCalledWith({ userPrompt: testMessage, explorer: expect.any(Explorer) });
expect(wrapper).toMatchSnapshot();
});
it("should not pass text with non enter key", () => {
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const textInput = wrapper.find(TextField).first();
// eslint-disable-next-line @typescript-eslint/no-empty-function
textInput.simulate("keydown", { key: "K", shiftKey: false, preventDefault: () => {} });
expect(useQueryCopilot.getState().chatMessages).toEqual([]);
expect(useQueryCopilot.getState().userPrompt).toEqual("");
expect(wrapper).toMatchSnapshot();
});
it("should not pass if no text", () => {
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const textInput = wrapper.find(TextField).first();
// eslint-disable-next-line @typescript-eslint/no-empty-function
textInput.simulate("keydown", { key: "Enter", shiftKey: false, preventDefault: () => {} });
expect(useQueryCopilot.getState().chatMessages).toEqual([]);
expect(useQueryCopilot.getState().userPrompt).toEqual("");
expect(wrapper).toMatchSnapshot();
});
it("should pass text with icon button", async () => {
useQueryCopilot.getState().setUserPrompt(testMessage);
const wrapper = shallow(<Footer explorer={new Explorer()} />);
const iconButton = wrapper.find(IconButton).first();
await act(async () => {
iconButton.simulate("click", {});
});
await Promise.resolve();
expect(SendQueryRequest).toHaveBeenCalledWith({ userPrompt: testMessage, explorer: expect.any(Explorer) });
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,77 +0,0 @@
import { IButtonStyles, IconButton, Image, Stack, TextField } from "@fluentui/react";
import { SendQueryRequest } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import HintIcon from "../../../../../images/Hint.svg";
import { SamplePrompts, SamplePromptsProps } from "../../Shared/SamplePrompts/SamplePrompts";
export const Footer: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
const { userPrompt, setUserPrompt, isSamplePromptsOpen, setIsSamplePromptsOpen, isGeneratingQuery } =
useQueryCopilot();
const promptStyles: IButtonStyles = {
root: { border: "5px", selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
};
const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen,
setIsSamplePromptsOpen: setIsSamplePromptsOpen,
setTextBox: setUserPrompt,
};
const handleEnterKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
startSentMessageProcess();
}
};
const startSentMessageProcess = async () => {
await SendQueryRequest({ userPrompt, explorer });
};
return (
<Stack
horizontal
horizontalAlign="end"
verticalAlign="end"
style={{
display: "flex",
alignItems: "center",
borderRadius: "20px",
background: "white",
padding: "5px",
margin: "5px",
}}
>
<Stack>
<Image src={HintIcon} styles={promptStyles} onClick={() => setIsSamplePromptsOpen(true)} />
<SamplePrompts sampleProps={sampleProps} />
</Stack>
<TextField
placeholder="Write your own prompt or ask a question"
value={userPrompt}
onChange={(_, newValue) => setUserPrompt(newValue)}
onKeyDown={handleEnterKeyPress}
multiline
resizable={false}
disabled={isGeneratingQuery}
styles={{
root: {
width: "100%",
height: "80px",
borderRadius: "20px",
padding: "8px",
border: "none",
outline: "none",
marginLeft: "10px",
},
fieldGroup: { border: "none" },
}}
/>
<IconButton disabled={isGeneratingQuery} iconProps={{ iconName: "Send" }} onClick={startSentMessageProcess} />
</Stack>
);
};

View File

@@ -1,511 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Footer snapshot test should not pass if no text 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
{
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src={{}}
styles={
{
"label": {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": {
"border": "5px",
"selectors": {
":hover": {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
{
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
{
"fieldGroup": {
"border": "none",
},
"root": {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value=""
/>
<CustomizedIconButton
disabled={null}
iconProps={
{
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;
exports[`Footer snapshot test should not pass text with non enter key 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
{
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src={{}}
styles={
{
"label": {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": {
"border": "5px",
"selectors": {
":hover": {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
{
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
{
"fieldGroup": {
"border": "none",
},
"root": {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value=""
/>
<CustomizedIconButton
disabled={null}
iconProps={
{
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;
exports[`Footer snapshot test should open sample prompts on button click 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
{
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src={{}}
styles={
{
"label": {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": {
"border": "5px",
"selectors": {
":hover": {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
{
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
{
"fieldGroup": {
"border": "none",
},
"root": {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value=""
/>
<CustomizedIconButton
disabled={null}
iconProps={
{
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;
exports[`Footer snapshot test should pass text with enter key 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
{
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src={{}}
styles={
{
"label": {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": {
"border": "5px",
"selectors": {
":hover": {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
{
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
{
"fieldGroup": {
"border": "none",
},
"root": {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value="test message"
/>
<CustomizedIconButton
disabled={null}
iconProps={
{
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;
exports[`Footer snapshot test should pass text with icon button 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
{
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src={{}}
styles={
{
"label": {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": {
"border": "5px",
"selectors": {
":hover": {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
{
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
{
"fieldGroup": {
"border": "none",
},
"root": {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value="test message"
/>
<CustomizedIconButton
disabled={null}
iconProps={
{
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;
exports[`Footer snapshot test should update user input 1`] = `
<Stack
horizontal={true}
horizontalAlign="end"
style={
{
"alignItems": "center",
"background": "white",
"borderRadius": "20px",
"display": "flex",
"margin": "5px",
"padding": "5px",
}
}
verticalAlign="end"
>
<Stack>
<Image
onClick={[Function]}
src={{}}
styles={
{
"label": {
"fontWeight": 400,
"paddingLeft": 8,
"textAlign": "left",
},
"root": {
"border": "5px",
"selectors": {
":hover": {
"outline": "1px dashed #605e5c",
},
},
},
}
}
/>
<SamplePrompts
sampleProps={
{
"isSamplePromptsOpen": false,
"setIsSamplePromptsOpen": [Function],
"setTextBox": [Function],
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={null}
multiline={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="Write your own prompt or ask a question"
resizable={false}
styles={
{
"fieldGroup": {
"border": "none",
},
"root": {
"border": "none",
"borderRadius": "20px",
"height": "80px",
"marginLeft": "10px",
"outline": "none",
"padding": "8px",
"width": "100%",
},
}
}
value=""
/>
<CustomizedIconButton
disabled={null}
iconProps={
{
"iconName": "Send",
}
}
onClick={[Function]}
/>
</Stack>
`;

View File

@@ -1,17 +0,0 @@
import { IconButton } from "@fluentui/react";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { Header } from "./Header";
describe("Header snapshot test", () => {
it("should close on button click", () => {
const wrapper = shallow(<Header />);
const iconButton = wrapper.find(IconButton).first();
iconButton.simulate("click", {});
expect(useQueryCopilot.getState().showCopilotSidebar).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,39 +0,0 @@
import { IconButton, Image, Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import CopilotIcon from "../../../../../images/CopilotSidebarLogo.svg";
export const Header: React.FC = (): JSX.Element => {
const { setShowCopilotSidebar } = useQueryCopilot();
return (
<Stack
style={{ margin: "15px 0px 0px 0px", padding: "5px", display: "flex", justifyContent: "space-between" }}
horizontal
verticalAlign="center"
>
<Stack horizontal verticalAlign="center">
<Image src={CopilotIcon} />
<Text style={{ marginLeft: "5px", fontWeight: "bold" }}>Copilot</Text>
<Text
style={{
background: "#f0f0f0",
fontSize: "10px",
padding: "2px 4px",
marginLeft: "5px",
borderRadius: "8px",
}}
>
Preview
</Text>
</Stack>
<IconButton
onClick={() => setShowCopilotSidebar(false)}
iconProps={{ iconName: "Cancel" }}
title="Exit"
ariaLabel="Exit"
style={{ color: "#424242", verticalAlign: "middle" }}
/>
</Stack>
);
};

View File

@@ -1,64 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header snapshot test should close on button click 1`] = `
<Stack
horizontal={true}
style={
{
"display": "flex",
"justifyContent": "space-between",
"margin": "15px 0px 0px 0px",
"padding": "5px",
}
}
verticalAlign="center"
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src={{}}
/>
<Text
style={
{
"fontWeight": "bold",
"marginLeft": "5px",
}
}
>
Copilot
</Text>
<Text
style={
{
"background": "#f0f0f0",
"borderRadius": "8px",
"fontSize": "10px",
"marginLeft": "5px",
"padding": "2px 4px",
}
}
>
Preview
</Text>
</Stack>
<CustomizedIconButton
ariaLabel="Exit"
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
style={
{
"color": "#424242",
"verticalAlign": "middle",
}
}
title="Exit"
/>
</Stack>
`;

View File

@@ -1,32 +0,0 @@
import { PrimaryButton } from "@fluentui/react";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { withHooks } from "jest-react-hooks-shallow";
import React from "react";
import { WelcomeSidebarModal } from "./WelcomeSidebarModal";
describe("Welcome Sidebar Modal snapshot test", () => {
it("should close on button click", () => {
withHooks(() => {
const wrapper = shallow(<WelcomeSidebarModal />);
const spy = jest.spyOn(localStorage, "setItem");
spy.mockClear();
const button = wrapper.find(PrimaryButton).first();
button.simulate("click", {});
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith("showWelcomeSidebar", "false");
expect(useQueryCopilot.getState().showWelcomeSidebar).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
});
it("should not reneder with local storage key", () => {
withHooks(() => {
window.localStorage.setItem("showWelcomeSidebar", "false");
const wrapper = shallow(<WelcomeSidebarModal />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -1,136 +0,0 @@
import { Image, Link, PrimaryButton, Stack, Text } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import Database from "../../../../../images/CopilotDatabase.svg";
import Flash from "../../../../../images/CopilotFlash.svg";
import CopilotSidebarWelcomeIllustration from "../../../../../images/CopilotSidebarWelcomeIllustration.svg";
import Thumb from "../../../../../images/CopilotThumb.svg";
export const WelcomeSidebarModal: React.FC = (): JSX.Element => {
const { showWelcomeSidebar, setShowWelcomeSidebar } = useQueryCopilot();
const hideModal = (): void => {
setShowWelcomeSidebar(false);
window.localStorage.setItem("showWelcomeSidebar", "false");
};
React.useEffect(() => {
const showWelcomeSidebar = window.localStorage.getItem("showWelcomeSidebar");
setShowWelcomeSidebar(showWelcomeSidebar && showWelcomeSidebar === "false" ? false : true);
}, []);
return (
showWelcomeSidebar && (
<Stack
style={{
width: "100%",
height: "100%",
overflow: "auto",
backgroundColor: "#FAFAFA",
flex: "1 0 100%",
}}
>
<div
style={{
margin: "20px 10px",
padding: "20px",
maxHeight: "100%",
boxSizing: "border-box",
borderRadius: "20px",
backgroundColor: "white",
}}
>
<Stack horizontalAlign="center" verticalAlign="center">
<Image src={CopilotSidebarWelcomeIllustration} />
</Stack>
<Stack>
<Stack.Item align="center" style={{ marginBottom: "10px" }}>
<Text className="title bold">Welcome to Copilot in CosmosDB</Text>
</Stack.Item>
<Stack.Item style={{ marginBottom: "15px" }}>
<Stack>
<Stack horizontal>
<Stack.Item align="start">
<Image src={Flash} />
</Stack.Item>
<Stack.Item align="center" style={{ marginLeft: "10px" }}>
<Text style={{ fontWeight: 600 }}>
Let copilot do the work for you
<br />
</Text>
</Stack.Item>
</Stack>
<Stack.Item style={{ textAlign: "start", marginLeft: "25px" }}>
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-learn-more">
Learn more
</Link>
</Text>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item style={{ marginBottom: "15px" }}>
<Stack>
<Stack horizontal>
<Stack.Item align="start">
<Image src={Thumb} />
</Stack.Item>
<Stack.Item align="center" style={{ marginLeft: "10px" }}>
<Text style={{ fontWeight: 600 }}>
Use your judgement
<br />
</Text>
</Stack.Item>
</Stack>
<Stack.Item style={{ textAlign: "start", marginLeft: "25px" }}>
<Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-preview-terms">
Read preview terms
</Link>
</Text>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item style={{ marginBottom: "15px" }}>
<Stack>
<Stack horizontal>
<Stack.Item align="start">
<Image src={Database} />
</Stack.Item>
<Stack.Item align="center" style={{ marginLeft: "10px" }}>
<Text style={{ fontWeight: 600 }}>
Copilot currently works only a sample database
<br />
</Text>
</Stack.Item>
</Stack>
<Stack.Item style={{ textAlign: "start", marginLeft: "25px" }}>
<Text>
Copilot is setup on a sample database we have configured for you at no cost
<br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-learn-more">
Learn more
</Link>
</Text>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
<Stack>
<Stack.Item align="center">
<PrimaryButton style={{ width: "224px", height: "32px" }} onClick={hideModal}>
Get Started
</PrimaryButton>
</Stack.Item>
</Stack>
</div>
</Stack>
)
);
};

View File

@@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Welcome Sidebar Modal snapshot test should close on button click 1`] = `""`;
exports[`Welcome Sidebar Modal snapshot test should not reneder with local storage key 1`] = `""`;

View File

@@ -1,55 +0,0 @@
import { Stack } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { withHooks } from "jest-react-hooks-shallow";
import React from "react";
import { QueryCopilotSidebar } from "./QueryCopilotSidebar";
describe("Query Copilot Sidebar snapshot test", () => {
const initialState = useQueryCopilot.getState();
beforeEach(() => {
useQueryCopilot.setState(initialState, true);
});
it("should render and set copilot used flag", () => {
withHooks(() => {
useQueryCopilot.getState().setShowCopilotSidebar(true);
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
expect(useQueryCopilot.getState().wasCopilotUsed).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
});
it("should render and not set copilot used flag", () => {
withHooks(() => {
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
expect(useQueryCopilot.getState().wasCopilotUsed).toBeFalsy();
expect(wrapper).toMatchSnapshot();
});
});
it("should render with chat messages", () => {
const message = "some test message";
useQueryCopilot.getState().setChatMessages([{ source: 0, message: message }]);
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
const messageContainer = wrapper.find(Stack).findWhere((x) => x.text() === message);
expect(messageContainer).toBeDefined();
expect(wrapper).toMatchSnapshot();
});
it("should render samples without messages", () => {
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
const sampleBubble = wrapper.find(SampleBubble);
expect(sampleBubble).toBeDefined();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,77 +0,0 @@
import { Stack } from "@fluentui/react";
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { ExplanationButton } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/Button/ExplanationButton";
import { ExplanationBubble } from "Explorer/QueryCopilot/V2/Bubbles/Explanation/ExplainationBubble";
import { OutputBubble } from "Explorer/QueryCopilot/V2/Bubbles/Output/OutputBubble";
import { RetrievingBubble } from "Explorer/QueryCopilot/V2/Bubbles/Retriveing/RetrievingBubble";
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
import { WelcomeBubble } from "Explorer/QueryCopilot/V2/Bubbles/Welcome/WelcomeBubble";
import { Footer } from "Explorer/QueryCopilot/V2/Footer/Footer";
import { Header } from "Explorer/QueryCopilot/V2/Header/Header";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { WelcomeSidebarModal } from "../Modal/WelcomeSidebarModal";
export const QueryCopilotSidebar: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
const { setWasCopilotUsed, showCopilotSidebar, chatMessages, isGeneratingQuery, showWelcomeSidebar } =
useQueryCopilot();
React.useEffect(() => {
if (showCopilotSidebar) {
setWasCopilotUsed(true);
}
}, []);
return (
<Stack style={{ width: "100%", height: "100%", backgroundColor: "#FAFAFA" }}>
<Header />
{showWelcomeSidebar ? (
<WelcomeSidebarModal />
) : (
<>
<Stack
style={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
overflowY: "auto",
}}
>
<WelcomeBubble />
{chatMessages.map((message, index) => {
switch (message.source) {
case 0:
return (
<Stack
key={index}
horizontalAlign="center"
tokens={{ padding: 8, childrenGap: 8 }}
style={{
backgroundColor: "#E0E7FF",
borderRadius: "8px",
margin: "5px 10px",
textAlign: "start",
}}
>
{message.message}
</Stack>
);
case 1:
return <OutputBubble key={index} copilotMessage={message} />;
case 2:
return <ExplanationBubble key={index} copilotMessage={message} />;
default:
return <></>;
}
})}
<RetrievingBubble />
<ExplanationButton />
{chatMessages.length === 0 && !isGeneratingQuery && <SampleBubble />}
</Stack>
<Footer explorer={explorer} />
</>
)}
</Stack>
);
};

View File

@@ -1,61 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Query Copilot Sidebar snapshot test should render and not set copilot used flag 1`] = `
<Stack
style={
{
"backgroundColor": "#FAFAFA",
"height": "100%",
"width": "100%",
}
}
>
<Header />
<WelcomeSidebarModal />
</Stack>
`;
exports[`Query Copilot Sidebar snapshot test should render and set copilot used flag 1`] = `
<Stack
style={
{
"backgroundColor": "#FAFAFA",
"height": "100%",
"width": "100%",
}
}
>
<Header />
<WelcomeSidebarModal />
</Stack>
`;
exports[`Query Copilot Sidebar snapshot test should render samples without messages 1`] = `
<Stack
style={
{
"backgroundColor": "#FAFAFA",
"height": "100%",
"width": "100%",
}
}
>
<Header />
<WelcomeSidebarModal />
</Stack>
`;
exports[`Query Copilot Sidebar snapshot test should render with chat messages 1`] = `
<Stack
style={
{
"backgroundColor": "#FAFAFA",
"height": "100%",
"width": "100%",
}
}
>
<Header />
<WelcomeSidebarModal />
</Stack>
`;

View File

@@ -1,198 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Query Copilot Carousel snapshot test should render when isOpen is true 1`] = `
<Modal
isOpen={true}
styles={
{
"main": {
"width": 880,
},
}
}
>
<Stack
style={
{
"padding": 16,
}
}
>
<Stack
horizontal={true}
horizontalAlign="space-between"
>
<Text
variant="xLarge"
>
What exactly is copilot?
</Text>
<CustomizedIconButton
iconProps={
{
"iconName": "Cancel",
}
}
onClick={[Function]}
/>
</Stack>
<Stack
style={
{
"marginTop": 8,
}
}
>
<Text
style={
{
"fontSize": 13,
}
}
>
A couple of lines about copilot and the background about it. The idea is to have some text to give context to the user.
</Text>
<Text
style={
{
"fontSize": 14,
"fontWeight": 600,
"marginTop": 16,
}
}
>
How do you use copilot
</Text>
<Text
style={
{
"fontSize": 13,
"marginTop": 8,
}
}
>
To generate queries , just describe the query you want and copilot will generate the query for you.Watch this video to learn more about how to use copilot.
</Text>
<Image
src={{}}
style={
{
"margin": "16px auto",
}
}
/>
<Text
style={
{
"fontSize": 14,
"fontWeight": 600,
}
}
>
What is copilot good at
</Text>
<Text
style={
{
"fontSize": 13,
"marginTop": 8,
}
}
>
A couple of lines about what copilot can do and its capablites with a link to
<StyledLinkBase
href=""
target="_blank"
>
documentation
</StyledLinkBase>
if possible.
</Text>
<Text
style={
{
"fontSize": 14,
"fontWeight": 600,
"marginTop": 16,
}
}
>
What are its limitations
</Text>
<Text
style={
{
"fontSize": 13,
"marginTop": 8,
}
}
>
A couple of lines about what copilot cant do and its limitations.
<StyledLinkBase
href=""
target="_blank"
>
Link to documentation
</StyledLinkBase>
</Text>
<Text
style={
{
"fontSize": 14,
"fontWeight": 600,
"marginTop": 16,
}
}
>
Disclaimer
</Text>
<Text
style={
{
"fontSize": 13,
"marginTop": 8,
}
}
>
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
<StyledLinkBase
href=""
target="_blank"
>
Read preview terms
</StyledLinkBase>
</Text>
</Stack>
<Separator
styles={
{
"root": {
"height": 1,
"padding": "16px 0",
"selectors": {
"::before": {
"background": undefined,
},
},
},
}
}
/>
<Stack
horizontal={true}
horizontalAlign="start"
verticalAlign="center"
>
<CustomizedPrimaryButton
disabled={false}
onClick={[Function]}
text="Next"
/>
</Stack>
</Stack>
</Modal>
`;

View File

@@ -1,171 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Prompt card snapshot test should render properly if isSelected is false 1`] = `
<Stack
horizontal={true}
style={
{
"border": "1px solid #F3F2F1",
"boxShadow": "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
"boxSizing": "border-box",
"height": 100,
"padding": "16px 0 16px 16px ",
"width": 650,
}
}
>
<StackItem
grow={1}
>
<Stack
horizontal={true}
>
<div>
<Text
style={
{
"background": "#F8FFF0",
"color": "#00A2AD",
"fontSize": 13,
}
}
>
Prompt
</Text>
</div>
<Text
style={
{
"fontSize": 13,
"marginLeft": 16,
}
}
>
TestHeader
</Text>
</Stack>
<Text
style={
{
"fontSize": 10,
"marginTop": 16,
}
}
>
TestDescription
</Text>
</StackItem>
<StackItem
style={
{
"marginLeft": 16,
}
}
>
<StyledChoiceGroup
onChange={[Function]}
options={
[
{
"key": "selected",
"text": "",
},
]
}
selectedKey=""
styles={
{
"flexContainer": {
"width": 36,
},
}
}
/>
</StackItem>
</Stack>
`;
exports[`Prompt card snapshot test should render properly if isSelected is true 1`] = `
<Stack
horizontal={true}
style={
{
"border": "1px solid #F3F2F1",
"boxShadow": "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
"boxSizing": "border-box",
"height": 100,
"padding": "16px 0 16px 16px ",
"width": 650,
}
}
>
<StackItem
grow={1}
>
<Stack
horizontal={true}
>
<div>
<Text
style={
{
"background": "#F8FFF0",
"color": "#00A2AD",
"fontSize": 13,
}
}
>
Prompt
</Text>
</div>
<Text
style={
{
"fontSize": 13,
"marginLeft": 16,
}
}
>
TestHeader
</Text>
</Stack>
<Text
style={
{
"fontSize": 10,
"marginTop": 16,
}
}
>
TestDescription
</Text>
</StackItem>
<StackItem
style={
{
"marginLeft": 16,
}
}
>
<StyledChoiceGroup
onChange={[Function]}
options={
[
{
"key": "selected",
"text": "",
},
]
}
selectedKey="selected"
styles={
{
"flexContainer": {
"width": 36,
},
}
}
/>
</StackItem>
</Stack>
`;

View File

@@ -1,49 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Query copilot tab snapshot test should render with initial input 1`] = `
<Stack
className="tab-pane"
style={
{
"width": "100%",
}
}
>
<div
style={
{
"height": "100%",
"overflowY": "auto",
}
}
>
<Stack
className="tabPaneContentContainer"
>
<t
customClassName=""
onDragEnd={null}
onDragStart={null}
onSecondaryPaneSizeChange={null}
percentage={true}
primaryIndex={0}
primaryMinSize={30}
secondaryMinSize={70}
vertical={true}
>
<EditorReact
ariaLabel="Editing Query"
content="SELECT * FROM c"
isReadOnly={false}
language="sql"
lineNumbers="on"
onContentChanged={[Function]}
onContentSelected={[Function]}
wordWrap="on"
/>
<QueryCopilotResults />
</t>
</Stack>
</div>
</Stack>
`;

View File

@@ -31,7 +31,6 @@ export const checkContainerExists = (databaseName: string, containerName: string
hasContainer(databaseName, containerName, useDatabases.getState().databases);
export enum SampleDataFile {
COPILOT = "Copilot",
FABRIC_SAMPLE_DATA = "FabricSampleData",
FABRIC_SAMPLE_VECTOR_DATA = "FabricSampleVectorData",
}
@@ -43,9 +42,6 @@ const containerSettings: {
indexingPolicy?: DataModels.IndexingPolicy;
};
} = {
[SampleDataFile.COPILOT]: {
partitionKeyString: "category",
},
[SampleDataFile.FABRIC_SAMPLE_DATA]: {
partitionKeyString: "categoryName",
},
@@ -120,11 +116,6 @@ export const createContainer = async (
export const importData = async (sampleDataFile: SampleDataFile, collection: ViewModels.Collection): Promise<void> => {
let documents: JSONObject[] = undefined;
switch (sampleDataFile) {
case SampleDataFile.COPILOT:
documents = (
await import(/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json")
).data;
break;
case SampleDataFile.FABRIC_SAMPLE_DATA:
documents = (await import(/* webpackChunkName: "fabricSampleData" */ "../../../sampleData/fabricSampleData.json"))
.default;

View File

@@ -21,7 +21,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { useCarousel } from "hooks/useCarousel";
import { usePostgres } from "hooks/usePostgres";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg";
@@ -215,12 +214,6 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
(state) => state.sampleDataResourceTokenCollection,
),
},
{
dispose: useQueryCopilot.subscribe(
() => setState({}),
(state) => state.copilotEnabled,
),
},
);
return () => {

View File

@@ -9,7 +9,6 @@ import {
makeStyles,
shorthands,
} from "@fluentui/react-components";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import MongoUtility from "Common/MongoUtility";
import { createDocument } from "Common/dataAccess/createDocument";
@@ -27,7 +26,7 @@ import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList";
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import {
ColumnsSelection,
FilterHistory,
@@ -360,10 +359,7 @@ export const getTabsButtons = ({
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled:
!getNewDocumentButtonState(editorState).enabled ||
!clientWriteEnabled ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
disabled: !getNewDocumentButtonState(editorState).enabled || !clientWriteEnabled,
id: NEW_DOCUMENT_BUTTON_ID,
});
}
@@ -378,10 +374,7 @@ export const getTabsButtons = ({
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled:
!getSaveNewDocumentButtonState(editorState).enabled ||
!clientWriteEnabled ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
disabled: !getSaveNewDocumentButtonState(editorState).enabled || !clientWriteEnabled,
id: SAVE_BUTTON_ID,
});
}
@@ -396,9 +389,7 @@ export const getTabsButtons = ({
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled:
!getDiscardNewDocumentChangesButtonState(editorState).enabled ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
disabled: !getDiscardNewDocumentChangesButtonState(editorState).enabled,
id: DISCARD_BUTTON_ID,
});
}
@@ -413,10 +404,7 @@ export const getTabsButtons = ({
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled:
!getSaveExistingDocumentButtonState(editorState).enabled ||
!clientWriteEnabled ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
disabled: !getSaveExistingDocumentButtonState(editorState).enabled || !clientWriteEnabled,
id: UPDATE_BUTTON_ID,
});
}
@@ -431,9 +419,7 @@ export const getTabsButtons = ({
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled:
!getDiscardExistingDocumentChangesButtonState(editorState).enabled ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
disabled: !getDiscardExistingDocumentChangesButtonState(editorState).enabled,
id: DISCARD_BUTTON_ID,
});
}
@@ -448,7 +434,7 @@ export const getTabsButtons = ({
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected() || !clientWriteEnabled,
disabled: !clientWriteEnabled,
id: DELETE_BUTTON_ID,
});
}
@@ -465,8 +451,7 @@ export const getTabsButtons = ({
hasPopup: true,
disabled:
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
!useClientWriteEnabled.getState().clientWriteEnabled ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
!useClientWriteEnabled.getState().clientWriteEnabled,
});
}
@@ -642,11 +627,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}),
);
const isQueryCopilotSampleContainer =
_collection?.isSampleCollection &&
_collection?.databaseId === QueryCopilotSampleDatabaseId &&
_collection?.id() === QueryCopilotSampleContainerId;
// For Mongo only
const [continuationToken, setContinuationToken] = useState<string>(undefined);
@@ -1400,16 +1380,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
// Fixes compile error error TS2741: Property 'throwIfAborted' is missing in type 'AbortSignal' but required in type 'import("/home/runner/work/cosmos-explorer/cosmos-explorer/node_modules/node-abort-controller/index").AbortSignal'.
options.abortSignal = _queryAbortController.signal;
return isQueryCopilotSampleContainer
? querySampleDocuments(query, options)
: queryDocuments(_collection.databaseId, _collection.id(), query, options);
return queryDocuments(_collection.databaseId, _collection.id(), query, options);
}, [
filterContent,
isPreferredApiMongoDB,
partitionKeyProperties,
partitionKey,
resourceTokenPartitionKey,
isQueryCopilotSampleContainer,
_collection,
selectedColumnIds,
]);
@@ -1567,11 +1544,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}
};
const _isQueryCopilotSampleContainer =
_collection?.isSampleCollection &&
_collection?.databaseId === QueryCopilotSampleDatabaseId &&
_collection?.id() === QueryCopilotSampleContainerId;
// Table config here
const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => {
const item: DocumentsTableComponentItem = documentId.tableFields || { id: documentId.id() };
@@ -1635,14 +1607,12 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
};
let loadDocument = (documentId: DocumentId) =>
(_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(_collection, documentId)).then(
(content) => {
initDocumentEditor(documentId, content);
readDocument(_collection, documentId).then((content) => {
initDocumentEditor(documentId, content);
// Update columns
setColumnDefinitionsFromDocument(content);
},
);
// Update columns
setColumnDefinitionsFromDocument(content);
});
const initDocumentEditor = (documentId: DocumentId, documentContent: unknown): void => {
if (documentId) {

View File

@@ -1,19 +1,12 @@
import { sendMessage } from "Common/MessageHandler";
import { ActionType, OpenQueryTab, TabKind } from "Contracts/ActionContracts";
import { MessageTypes } from "Contracts/MessageTypes";
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
import { userContext } from "UserContext";
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer";
import {
IQueryTabComponentProps,
ITabAccessor,
QueryTabComponent,
QueryTabCopilotComponent,
} from "../../Tabs/QueryTab/QueryTabComponent";
import { IQueryTabComponentProps, ITabAccessor, QueryTabComponent } from "../../Tabs/QueryTab/QueryTabComponent";
import TabsBase from "../TabsBase";
export interface IQueryTabProps {
@@ -80,13 +73,7 @@ export class NewQueryTab extends TabsBase {
}
public render(): JSX.Element {
return userContext.apiType === "SQL" ? (
<CopilotProvider>
<QueryTabCopilotComponent {...this.iQueryTabComponentProps} />
</CopilotProvider>
) : (
<QueryTabComponent {...this.iQueryTabComponentProps} />
);
return <QueryTabComponent {...this.iQueryTabComponentProps} />;
}
public onActivate(): void {

View File

@@ -1,60 +1,21 @@
import { fireEvent, render } from "@testing-library/react";
import { CollectionTabKind } from "Contracts/ViewModels";
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import { CopilotSubComponentNames } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import {
IQueryTabComponentProps,
QueryTabComponent,
QueryTabCopilotComponent,
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
import TabsBase from "Explorer/Tabs/TabsBase";
import { AppStateComponentNames, StorePath } from "Shared/AppStatePersistenceUtility";
import { updateUserContext, userContext } from "UserContext";
import { mount } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs";
import { render } from "@testing-library/react";
import { IQueryTabComponentProps, QueryTabComponent } from "Explorer/Tabs/QueryTab/QueryTabComponent";
import React from "react";
jest.mock("Explorer/Controls/Editor/EditorReact");
const loadState = (path: StorePath) => {
if (
path.componentName === AppStateComponentNames.QueryCopilot &&
path.subComponentName === CopilotSubComponentNames.toggleStatus
) {
return true;
} else {
return undefined;
}
};
jest.mock("Shared/AppStatePersistenceUtility", () => ({
loadState,
AppStateComponentNames: {
QueryCopilot: "QueryCopilot",
},
loadState: jest.fn(),
AppStateComponentNames: {},
readSubComponentState: jest.fn(),
}));
describe("QueryTabComponent", () => {
const mockStore = useQueryCopilot.getState();
beforeEach(() => {
mockStore.showCopilotSidebar = false;
mockStore.setShowCopilotSidebar = jest.fn();
});
afterEach(() => jest.clearAllMocks());
it("should launch conversational Copilot when ALT+C is pressed and when copilot version is 3", () => {
updateUserContext({
features: {
...userContext.features,
copilotVersion: "v3.0",
},
});
it("should render without crashing", () => {
const propsMock: Readonly<IQueryTabComponentProps> = {
collection: { databaseId: "CopilotSampleDB", id: () => "CopilotContainer" },
collection: { databaseId: "testDb", id: () => "testContainer" },
onTabAccessor: () => jest.fn(),
isExecutionError: false,
tabId: "mockTabId",
@@ -64,49 +25,6 @@ describe("QueryTabComponent", () => {
} as unknown as IQueryTabComponentProps;
const { container } = render(<QueryTabComponent {...propsMock} />);
const launchCopilotButton = container.querySelector('[data-test="QueryTab/ResultsPane/ExecuteCTA"]');
fireEvent.keyDown(launchCopilotButton, { key: "c", altKey: true });
expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true);
});
it("copilot should be enabled by default when tab is active", () => {
updateUserContext({
databaseAccount: {
name: "name",
properties: undefined,
id: "",
location: "",
type: "",
kind: "",
},
});
useQueryCopilot.getState().setCopilotEnabled(true);
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
const activeTab = new TabsBase({
tabKind: CollectionTabKind.Query,
title: "Query",
tabPath: "",
});
activeTab.tabId = "mockTabId";
useTabs.getState().activeTab = activeTab;
const propsMock: Readonly<IQueryTabComponentProps> = {
collection: { databaseId: "CopilotUserDb", id: () => "CopilotUserContainer" },
onTabAccessor: () => jest.fn(),
isExecutionError: false,
tabId: "mockTabId",
tabsBaseInstance: {
updateNavbarWithTabsButtons: () => jest.fn(),
},
} as unknown as IQueryTabComponentProps;
const container = mount(
<CopilotProvider>
<QueryTabCopilotComponent {...propsMock} />
</CopilotProvider>,
);
expect(container.find(QueryCopilotPromptbar).exists()).toBe(false);
expect(container).toBeTruthy();
});
});

View File

@@ -7,32 +7,21 @@ import { SplitterDirection } from "Common/Splitter";
import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog";
import { monaco } from "Explorer/LazyMonaco";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { QueryTabStyles, useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction } from "KeyboardShortcuts";
import { Keys, t } from "Localization";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { Allotment } from "allotment";
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { TabsState, useTabs } from "hooks/useTabs";
import { useMonacoTheme } from "hooks/useTheme";
import React, { Fragment, createRef } from "react";
import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format";
import create from "zustand";
//TODO: Uncomment next two lines when query copilot is reinstated in DE
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
@@ -46,7 +35,6 @@ import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import * as QueryUtils from "../../../Utils/QueryUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
@@ -109,9 +97,6 @@ export interface IQueryTabComponentProps {
isPreferredApiMongoDB?: boolean;
monacoEditorSetting?: string;
viewModelcollection?: ViewModels.Collection;
copilotEnabled?: boolean;
isSampleCopilotActive?: boolean;
copilotStore?: Partial<QueryCopilotState>;
splitterDirection?: "horizontal" | "vertical";
queryViewSizePercent?: number;
onUpdatePersistedState: (state: {
@@ -130,10 +115,7 @@ interface IQueryTabStates {
queryResults: ViewModels.QueryResults;
isExecutionError: boolean;
isExecuting: boolean;
showCopilotSidebar: boolean;
queryCopilotGeneratedQuery: string;
cancelQueryTimeoutID: NodeJS.Timeout;
copilotActive: boolean;
currentTabActive: boolean;
queryResultsView: SplitterDirection;
errors?: QueryError[];
@@ -141,23 +123,6 @@ interface IQueryTabStates {
queryViewSizePercent: number;
}
export const QueryTabCopilotComponent = (props: IQueryTabComponentProps): any => {
const styles = useQueryTabStyles();
const monacoTheme = useMonacoTheme();
const copilotStore = useCopilotStore();
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
const queryTabProps = {
...props,
copilotEnabled:
useQueryCopilot().copilotEnabled &&
(useQueryCopilot().copilotUserDBEnabled || (isSampleCopilotActive && !!userContext.sampleDataConnectionInfo)),
isSampleCopilotActive: isSampleCopilotActive,
copilotStore: copilotStore,
};
return <QueryTabComponentImpl styles={styles} monacoTheme={monacoTheme} {...queryTabProps} />;
};
export const QueryTabComponent = (props: IQueryTabComponentProps): any => {
const styles = useQueryTabStyles();
const monacoTheme = useMonacoTheme();
@@ -176,11 +141,9 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
public queryEditorId: string;
public executeQueryButton: Button;
public saveQueryButton: Button;
public launchCopilotButton: Button;
public splitterId: string;
public isPreferredApiMongoDB: boolean;
public isCloseClicked: boolean;
public isCopilotTabActive: boolean;
private _iterator: MinimalQueryIterator;
private queryAbortController: AbortController;
queryEditor: React.RefObject<EditorReact>;
@@ -198,10 +161,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
errors: [],
isExecutionError: this.props.isExecutionError,
isExecuting: false,
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
cancelQueryTimeoutID: undefined,
copilotActive: this._queryCopilotActive(),
currentTabActive: true,
queryResultsView:
props.splitterDirection === "vertical" ? SplitterDirection.Vertical : SplitterDirection.Horizontal,
@@ -211,7 +171,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
this.splitterId = this.props.tabId + "_splitter";
this.queryEditorId = `queryeditor${this.props.tabId}`;
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
this.isCopilotTabActive = userContext.features.copilotVersion === "v3.0";
this.executeQueryButton = {
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
visible: true,
@@ -223,11 +182,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
visible: isSaveQueryBtnEnabled,
};
this.launchCopilotButton = {
enabled: userContext.apiType === "SQL" && true,
visible: userContext.apiType === "SQL" && true,
};
this.props.tabsBaseInstance.updateNavbarWithTabsButtons();
props.onTabAccessor({
onTabClickEvent: this.onTabClick.bind(this),
@@ -253,18 +207,8 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}, QueryTabComponentImpl.DEBOUNCE_DELAY_MS);
};
private _queryCopilotActive(): boolean {
if (this.props.copilotEnabled) {
return readCopilotToggleStatus(userContext.databaseAccount);
}
return false;
}
public onCloseClick(isClicked: boolean): void {
this.isCloseClicked = isClicked;
if (useQueryCopilot.getState().wasCopilotUsed && this.isCopilotTabActive) {
useQueryCopilot.getState().resetQueryCopilotStates();
}
}
public getCurrentEditorQuery(): string {
@@ -289,16 +233,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
setTimeout(async () => {
await this._executeQueryDocumentsPage(0);
}, 100); // TODO: Revert this
if (this.state.copilotActive) {
const query = this.state.sqlQueryEditorContent.split("\r\n")?.pop();
const isqueryEdited = this.props.copilotStore.generatedQuery && this.props.copilotStore.generatedQuery !== query;
if (isqueryEdited) {
TelemetryProcessor.traceMark(Action.QueryEdited, {
databaseName: this.props.collection.databaseId,
collectionId: this.props.collection.id(),
});
}
}
};
public onDownloadQueryClick = (): void => {
@@ -321,10 +255,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
.openSidePanel(t(Keys.tabs.query.saveQuery), <SaveQueryPane explorer={this.props.collection.container} />);
};
public launchQueryCopilotChat = (): void => {
useQueryCopilot.getState().setShowCopilotSidebar(!useQueryCopilot.getState().showCopilotSidebar);
};
public onSavedQueriesClick = (): void => {
useSidePanel
.getState()
@@ -346,12 +276,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
});
}
public handleCopilotKeyDown = (event: KeyboardEvent): void => {
if (this.isCopilotTabActive && event.altKey && event.key === "c") {
this.launchQueryCopilotChat();
}
};
public onToggleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): boolean => {
if (event.key === NormalizedEventKey.LeftArrow) {
this.toggleResult();
@@ -485,9 +409,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
iconSrc: ExecuteQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.EXECUTE_ITEM,
onCommandClick: this.props.isSampleCopilotActive
? () => OnExecuteQueryClick(this.props.copilotStore)
: this.onExecuteQueryClick,
onCommandClick: this.onExecuteQueryClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
@@ -524,56 +446,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
});
}
//TODO: Uncomment next section when query copilot is reinstated in DE
// if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
// const mainButtonLabel = "Launch Copilot";
// const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
// const copilotSettingLabel = "Copilot settings";
// const openCopilotChatButton: CommandButtonComponentProps = {
// iconAlt: chatPaneLabel,
// onCommandClick: this.launchQueryCopilotChat,
// commandButtonLabel: chatPaneLabel,
// ariaLabel: chatPaneLabel,
// hasPopup: false,
// };
// const copilotSettingsButton: CommandButtonComponentProps = {
// iconAlt: copilotSettingLabel,
// onCommandClick: () => undefined,
// commandButtonLabel: copilotSettingLabel,
// ariaLabel: copilotSettingLabel,
// hasPopup: false,
// };
// const launchCopilotButton: CommandButtonComponentProps = {
// iconSrc: LaunchCopilot,
// iconAlt: mainButtonLabel,
// onCommandClick: this.launchQueryCopilotChat,
// commandButtonLabel: mainButtonLabel,
// ariaLabel: mainButtonLabel,
// hasPopup: false,
// children: [openCopilotChatButton, copilotSettingsButton],
// };
// buttons.push(launchCopilotButton);
// }
//TODO: Uncomment next section when query copilot is reinstated in DE
// if (this.props.copilotEnabled) {
// const toggleCopilotButton: CommandButtonComponentProps = {
// iconSrc: QueryCommandIcon,
// iconAlt: "Query Advisor",
// keyboardAction: KeyboardAction.TOGGLE_COPILOT,
// onCommandClick: () => {
// this._toggleCopilot(!this.state.copilotActive);
// },
// commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
// ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
// hasPopup: false,
// };
// buttons.push(toggleCopilotButton);
// }
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
const label = t(Keys.tabs.query.cancelQuery);
buttons.push({
@@ -626,34 +498,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}, 100);
}
private _toggleCopilot = (active: boolean) => {
this.setState({ copilotActive: active });
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
saveCopilotToggleStatus(userContext.databaseAccount, active);
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
databaseName: this.props.collection.databaseId,
collectionId: this.props.collection.id(),
});
};
componentDidUpdate = (_prevProps: IQueryTabComponentProps, prevState: IQueryTabStates): void => {
if (prevState.copilotActive !== this.state.copilotActive) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
};
public onChangeContent(newContent: string): void {
// The copilot store's active query takes precedence over the local state,
// and we can't update both states in a single operation.
// So, we update the copilot store's state first, then update the local state.
if (this.state.copilotActive) {
this.props.copilotStore?.setQuery(newContent);
}
this.setState(
{
sqlQueryEditorContent: newContent,
queryCopilotGeneratedQuery: "",
// Clear the markers when the user edits the document.
modelMarkers: [],
@@ -692,28 +540,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
});
}
if (this.isCopilotTabActive) {
selectedContent.trim().length > 0
? useQueryCopilot.getState().setSelectedQuery(selectedContent)
: useQueryCopilot.getState().setSelectedQuery("");
}
if (this.state.copilotActive) {
this.props.copilotStore?.setSelectedQuery(selectedContent);
}
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
public getEditorContent(): string {
if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) {
return this.state.queryCopilotGeneratedQuery;
}
if (this.state.copilotActive) {
return this.props.copilotStore?.query;
}
return this.state.sqlQueryEditorContent;
}
@@ -721,7 +551,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
}
private unsubscribeCopilotSidebar: () => void;
private unsubscribeClientWriteEnabled: () => void;
componentDidMount(): void {
@@ -738,7 +567,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
});
useCommandBar.getState().setContextButtons(this.getTabsButtons());
document.addEventListener("keydown", this.handleCopilotKeyDown);
this.unsubscribeClientWriteEnabled = useClientWriteEnabled.subscribe(() => {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
@@ -746,7 +574,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}
componentWillUnmount(): void {
document.removeEventListener("keydown", this.handleCopilotKeyDown);
if (this.unsubscribeClientWriteEnabled) {
this.unsubscribeClientWriteEnabled();
}
@@ -757,15 +584,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
return (
<Fragment>
<CosmosFluentProvider id={this.props.tabId} className={this.props.styles.queryTab} role="tabpanel">
{/*TODO: Uncomment this section when query copilot is reinstated in DE
{this.props.copilotEnabled && this.state.currentTabActive && this.state.copilotActive && (
<QueryCopilotPromptbar
explorer={this.props.collection.container}
toggleCopilot={this._toggleCopilot}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
></QueryCopilotPromptbar>
)} */}
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
<Allotment
key={vertical.toString()}
@@ -799,64 +617,27 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
/>
</Allotment.Pane>
<Allotment.Pane>
{this.props.isSampleCopilotActive ? (
<QueryResultSection
isMongoDB={this.props.isPreferredApiMongoDB}
queryEditorContent={this.state.sqlQueryEditorContent}
errors={this.props.copilotStore?.errors}
isExecuting={this.props.copilotStore?.isExecuting}
queryResults={this.props.copilotStore?.queryResults}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage(
firstItemIndex,
this.props.copilotStore.queryIterator,
this.props.copilotStore,
)
}
/>
) : (
<QueryResultSection
isMongoDB={this.props.isPreferredApiMongoDB}
queryEditorContent={this.state.sqlQueryEditorContent}
errors={this.state.errors}
isExecuting={this.state.isExecuting}
queryResults={this.state.queryResults}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
executeQueryDocumentsPage={(firstItemIndex: number) =>
this._executeQueryDocumentsPage(firstItemIndex)
}
/>
)}
<QueryResultSection
isMongoDB={this.props.isPreferredApiMongoDB}
queryEditorContent={this.state.sqlQueryEditorContent}
errors={this.state.errors}
isExecuting={this.state.isExecuting}
queryResults={this.state.queryResults}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
executeQueryDocumentsPage={(firstItemIndex: number) => this._executeQueryDocumentsPage(firstItemIndex)}
/>
</Allotment.Pane>
</Allotment>
</CosmosFluentProvider>
{this.props.copilotEnabled && this.props.copilotStore?.showFeedbackModal && (
<QueryCopilotFeedbackModal
explorer={this.props.collection.container}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
mode={this.props.isSampleCopilotActive ? "Sample" : "User"}
/>
)}
</Fragment>
);
}
render(): JSX.Element {
const shouldScaleElements = this.state.showCopilotSidebar && this.isCopilotTabActive;
return (
<div data-test="QueryTab" style={{ display: "flex", flexDirection: "row", height: "100%" }}>
<div style={{ width: shouldScaleElements ? "70%" : "100%", height: "100%" }}>
{this.getEditorAndQueryResult()}
</div>
{shouldScaleElements && (
<div style={{ width: "30%", height: "100%" }}>
<QueryCopilotSidebar explorer={this.props.collection.container} />
</div>
)}
<div style={{ width: "100%", height: "100%" }}>{this.getEditorAndQueryResult()}</div>
</div>
);
}

View File

@@ -42,7 +42,6 @@ import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
import create from "zustand";
import { client } from "../../../Common/CosmosClient";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { sampleDataClient } from "../../../Common/SampleDataClient";
import { ResultsViewProps } from "./QueryResultSection";
import { useIndexAdvisorStyles } from "./StylesAdvisor";
enum ResultsTabs {
@@ -583,8 +582,7 @@ export const IndexAdvisorTab: React.FC<{
query: queryEditorContent,
};
// Use sampleDataClient for CopilotSampleDB, regular client for other databases
const cosmosClient = databaseId === "CopilotSampleDB" ? sampleDataClient() : client();
const cosmosClient = client();
const sdkResponse = await cosmosClient
.database(databaseId)

Some files were not shown because too many files have changed in this diff Show More