Copilot user db (#1672)
* Implement copilot for user database * Fix minor bugs * fix bugs * Add user database copilot * Add placeholder text on copilot * Add AFEC adn killswitch * Add new v2 sampledatabase endpoint * Add telemetry * fix telemetry bug * Add query edited telemetry * add authorization header * Add back to the staging env for phoenix * point to stage for phoenix * Preview commit for test env * Preview link for staging * change the staging url * fix lint, unit tests * fix lint, unit tests * fix formatting
This commit is contained in:
parent
a5e1b37ba6
commit
0e124f4881
|
@ -171,6 +171,7 @@ export class Areas {
|
||||||
public static Tab: string = "Tab";
|
public static Tab: string = "Tab";
|
||||||
public static ShareDialog: string = "Share Access Dialog";
|
public static ShareDialog: string = "Share Access Dialog";
|
||||||
public static Notebook: string = "Notebook";
|
public static Notebook: string = "Notebook";
|
||||||
|
public static Copilot: string = "Copilot";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HttpHeaders {
|
export class HttpHeaders {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { JunoEndpoints } from "Common/Constants";
|
||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
|
@ -78,7 +79,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
||||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
||||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
|
|
|
@ -457,8 +457,11 @@ export interface ContainerInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProvisionData {
|
export interface IProvisionData {
|
||||||
cosmosEndpoint: string;
|
cosmosEndpoint?: string;
|
||||||
poolId: string;
|
poolId: string;
|
||||||
|
databaseId?: string;
|
||||||
|
containerId?: string;
|
||||||
|
mode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IContainerData {
|
export interface IContainerData {
|
||||||
|
@ -601,3 +604,14 @@ export enum PhoenixErrorType {
|
||||||
PhoenixFlightFallback = "PhoenixFlightFallback",
|
PhoenixFlightFallback = "PhoenixFlightFallback",
|
||||||
UserMissingPermissionsError = "UserMissingPermissionsError",
|
UserMissingPermissionsError = "UserMissingPermissionsError",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CopilotEnabledConfiguration {
|
||||||
|
isEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeatureRegistration {
|
||||||
|
name: string;
|
||||||
|
properties: {
|
||||||
|
state: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||||
|
|
||||||
public componentDidUpdate(previous: EditorReactProps) {
|
public componentDidUpdate(previous: EditorReactProps) {
|
||||||
if (this.props.content !== previous.content) {
|
if (this.props.content !== previous.content) {
|
||||||
this.editor.setValue(this.props.content);
|
this.editor?.setValue(this.props.content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||||
|
|
||||||
this.rootNode.innerHTML = "";
|
this.rootNode.innerHTML = "";
|
||||||
const monaco = await loadMonaco();
|
const monaco = await loadMonaco();
|
||||||
createCallback(monaco.editor.create(this.rootNode, options));
|
createCallback(monaco?.editor?.create(this.rootNode, options));
|
||||||
|
|
||||||
if (this.rootNode.innerHTML) {
|
if (this.rootNode.innerHTML) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
|
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
|
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
|
@ -92,7 +93,7 @@ export default class Explorer {
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||||
private phoenixClient: PhoenixClient;
|
public phoenixClient: PhoenixClient;
|
||||||
constructor() {
|
constructor() {
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
|
@ -411,7 +412,7 @@ export default class Explorer {
|
||||||
this._isInitializingNotebooks = false;
|
this._isInitializingNotebooks = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async allocateContainer(poolId: PoolIdType): Promise<void> {
|
public async allocateContainer(poolId: PoolIdType, mode?: string): Promise<void> {
|
||||||
const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false;
|
const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false;
|
||||||
const notebookServerInfo = shouldUseNotebookStates
|
const notebookServerInfo = shouldUseNotebookStates
|
||||||
? useNotebook.getState().notebookServerInfo
|
? useNotebook.getState().notebookServerInfo
|
||||||
|
@ -425,10 +426,6 @@ export default class Explorer {
|
||||||
(notebookServerInfo === undefined ||
|
(notebookServerInfo === undefined ||
|
||||||
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
|
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
|
||||||
) {
|
) {
|
||||||
const provisionData: IProvisionData = {
|
|
||||||
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
|
|
||||||
poolId: shouldUseNotebookStates ? undefined : poolId,
|
|
||||||
};
|
|
||||||
const connectionStatus: ContainerConnectionInfo = {
|
const connectionStatus: ContainerConnectionInfo = {
|
||||||
status: ConnectionStatusType.Connecting,
|
status: ConnectionStatusType.Connecting,
|
||||||
};
|
};
|
||||||
|
@ -436,14 +433,26 @@ export default class Explorer {
|
||||||
shouldUseNotebookStates && useNotebook.getState().setConnectionInfo(connectionStatus);
|
shouldUseNotebookStates && useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
|
|
||||||
let connectionInfo;
|
let connectionInfo;
|
||||||
|
let provisionData: IProvisionData;
|
||||||
try {
|
try {
|
||||||
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
|
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
|
||||||
dataExplorerArea: Areas.Notebook,
|
dataExplorerArea: Areas.Notebook,
|
||||||
});
|
});
|
||||||
shouldUseNotebookStates
|
if (shouldUseNotebookStates) {
|
||||||
? useNotebook.getState().setIsAllocating(true)
|
useNotebook.getState().setIsAllocating(true);
|
||||||
: useQueryCopilot.getState().setIsAllocatingContainer(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||||
if (!connectionInfo?.data?.phoenixServiceUrl) {
|
if (!connectionInfo?.data?.phoenixServiceUrl) {
|
||||||
throw new Error(`PhoenixServiceUrl is invalid!`);
|
throw new Error(`PhoenixServiceUrl is invalid!`);
|
||||||
|
@ -459,19 +468,21 @@ export default class Explorer {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
errorStack: getErrorStack(error),
|
errorStack: getErrorStack(error),
|
||||||
});
|
});
|
||||||
connectionStatus.status = ConnectionStatusType.Failed;
|
if (shouldUseNotebookStates) {
|
||||||
shouldUseNotebookStates
|
connectionStatus.status = ConnectionStatusType.Failed;
|
||||||
? useNotebook.getState().resetContainerConnection(connectionStatus)
|
shouldUseNotebookStates
|
||||||
: useQueryCopilot.getState().resetContainerConnection();
|
? useNotebook.getState().resetContainerConnection(connectionStatus)
|
||||||
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
|
: useQueryCopilot.getState().resetContainerConnection();
|
||||||
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
|
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
|
||||||
} else {
|
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
|
||||||
useDialog
|
} else {
|
||||||
.getState()
|
useDialog
|
||||||
.showOkModalDialog(
|
.getState()
|
||||||
"Connection Failed",
|
.showOkModalDialog(
|
||||||
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
|
"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;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -485,11 +496,11 @@ export default class Explorer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setNotebookInfo(
|
public async setNotebookInfo(
|
||||||
shouldUseNotebookStates: boolean,
|
shouldUseNotebookStates: boolean,
|
||||||
connectionInfo: IResponse<IPhoenixServiceInfo>,
|
connectionInfo: IResponse<IPhoenixServiceInfo>,
|
||||||
connectionStatus: DataModels.ContainerConnectionInfo,
|
connectionStatus: DataModels.ContainerConnectionInfo,
|
||||||
) {
|
): Promise<void> {
|
||||||
const containerData = {
|
const containerData = {
|
||||||
forwardingId: connectionInfo.data.forwardingId,
|
forwardingId: connectionInfo.data.forwardingId,
|
||||||
dbAccountName: userContext.databaseAccount.name,
|
dbAccountName: userContext.databaseAccount.name,
|
||||||
|
@ -510,6 +521,7 @@ export default class Explorer {
|
||||||
shouldUseNotebookStates
|
shouldUseNotebookStates
|
||||||
? useNotebook.getState().setNotebookServerInfo(noteBookServerInfo)
|
? useNotebook.getState().setNotebookServerInfo(noteBookServerInfo)
|
||||||
: useQueryCopilot.getState().setNotebookServerInfo(noteBookServerInfo);
|
: useQueryCopilot.getState().setNotebookServerInfo(noteBookServerInfo);
|
||||||
|
|
||||||
shouldUseNotebookStates &&
|
shouldUseNotebookStates &&
|
||||||
this.notebookManager?.notebookClient
|
this.notebookManager?.notebookClient
|
||||||
.getMemoryUsage()
|
.getMemoryUsage()
|
||||||
|
@ -1372,6 +1384,16 @@ export default class Explorer {
|
||||||
await this.refreshSampleData();
|
await this.refreshSampleData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async configureCopilot(): Promise<void> {
|
||||||
|
if (userContext.apiType !== "SQL") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const copilotEnabled = await getCopilotEnabled();
|
||||||
|
const copilotUserDBEnabled = await isCopilotFeatureRegistered(userContext.subscriptionId);
|
||||||
|
useQueryCopilot.getState().setCopilotEnabled(copilotEnabled);
|
||||||
|
useQueryCopilot.getState().setCopilotUserDBEnabled(copilotUserDBEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
public async refreshSampleData(): Promise<void> {
|
public async refreshSampleData(): Promise<void> {
|
||||||
if (!userContext.sampleDataConnectionInfo) {
|
if (!userContext.sampleDataConnectionInfo) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||||
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
|
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
|
||||||
|
@ -337,13 +334,8 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||||
iconSrc: AddSqlQueryIcon,
|
iconSrc: AddSqlQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||||
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
|
|
||||||
} else {
|
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
|
||||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
IDropdownOption,
|
IDropdownOption,
|
||||||
IDropdownStyles,
|
IDropdownStyles,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||||
|
@ -57,7 +58,11 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
||||||
},
|
},
|
||||||
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||||
btn.onCommandClick(ev);
|
btn.onCommandClick(ev);
|
||||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label });
|
let copilotEnabled = false;
|
||||||
|
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
|
||||||
|
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
|
||||||
|
}
|
||||||
|
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
|
||||||
},
|
},
|
||||||
key: `${btn.commandButtonLabel}${index}`,
|
key: `${btn.commandButtonLabel}${index}`,
|
||||||
text: label,
|
text: label,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Checkbox, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react";
|
import { Checkbox, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
|
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { getUserEmail } from "Utils/UserUtils";
|
import { getUserEmail } from "Utils/UserUtils";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
jest.mock("Utils/UserUtils");
|
jest.mock("Utils/UserUtils");
|
||||||
|
@ -13,21 +13,49 @@ jest.mock("Utils/UserUtils");
|
||||||
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
|
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
|
||||||
SubmitFeedback as jest.Mock;
|
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", () => {
|
describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockUseCopilotStore.mockReturnValue(mockReturnValue);
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
it("shoud render and match snapshot", () => {
|
it("shoud render and match snapshot", () => {
|
||||||
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
|
mockUseCopilotStore.mockReturnValue({
|
||||||
|
...mockReturnValue,
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
showFeedbackModal: true,
|
||||||
|
});
|
||||||
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotFeedbackModal
|
||||||
|
explorer={new Explorer()}
|
||||||
|
databaseId="CopilotUserDb"
|
||||||
|
containerId="CopilotUserContainer"
|
||||||
|
mode="User"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
expect(wrapper.props().isOpen).toBeTruthy();
|
expect(wrapper.props().isOpen).toBeTruthy();
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should close on cancel click", () => {
|
it("should close on cancel click", () => {
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotFeedbackModal
|
||||||
|
explorer={new Explorer()}
|
||||||
|
databaseId="CopilotUserDb"
|
||||||
|
containerId="CopilotUserContainer"
|
||||||
|
mode="User"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
const cancelButton = wrapper.find(IconButton);
|
const cancelButton = wrapper.find(IconButton);
|
||||||
cancelButton.simulate("click");
|
cancelButton.simulate("click");
|
||||||
|
@ -38,7 +66,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get user unput", () => {
|
it("should get user unput", () => {
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotFeedbackModal
|
||||||
|
explorer={new Explorer()}
|
||||||
|
databaseId="CopilotUserDb"
|
||||||
|
containerId="CopilotUserContainer"
|
||||||
|
mode="User"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const testUserInput = "test user input";
|
const testUserInput = "test user input";
|
||||||
|
|
||||||
const userInput = wrapper.find(TextField).first();
|
const userInput = wrapper.find(TextField).first();
|
||||||
|
@ -49,7 +84,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should record user contact choice no", () => {
|
it("should record user contact choice no", () => {
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotFeedbackModal
|
||||||
|
explorer={new Explorer()}
|
||||||
|
databaseId="CopilotUserDb"
|
||||||
|
containerId="CopilotUserContainer"
|
||||||
|
mode="User"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const contactAllowed = wrapper.find(ChoiceGroup);
|
const contactAllowed = wrapper.find(ChoiceGroup);
|
||||||
|
|
||||||
contactAllowed.simulate("change", {}, { key: "no" });
|
contactAllowed.simulate("change", {}, { key: "no" });
|
||||||
|
@ -60,7 +102,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should record user contact choice yes", () => {
|
it("should record user contact choice yes", () => {
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotFeedbackModal
|
||||||
|
explorer={new Explorer()}
|
||||||
|
databaseId="CopilotUserDb"
|
||||||
|
containerId="CopilotUserContainer"
|
||||||
|
mode="User"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const contactAllowed = wrapper.find(ChoiceGroup);
|
const contactAllowed = wrapper.find(ChoiceGroup);
|
||||||
|
|
||||||
contactAllowed.simulate("change", {}, { key: "yes" });
|
contactAllowed.simulate("change", {}, { key: "yes" });
|
||||||
|
@ -71,7 +120,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not render dont show again button", () => {
|
it("should not render dont show again button", () => {
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotFeedbackModal
|
||||||
|
explorer={new Explorer()}
|
||||||
|
databaseId="CopilotUserDb"
|
||||||
|
containerId="CopilotUserContainer"
|
||||||
|
mode="User"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
const dontShowAgain = wrapper.find(Checkbox);
|
const dontShowAgain = wrapper.find(Checkbox);
|
||||||
|
|
||||||
|
@ -80,8 +136,19 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render dont show again button and check it ", () => {
|
it("should render dont show again button and check it ", () => {
|
||||||
useQueryCopilot.getState().openFeedbackModal("test query", true, "test prompt");
|
mockUseCopilotStore.mockReturnValue({
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
...mockReturnValue,
|
||||||
|
showFeedbackModal: true,
|
||||||
|
likeQuery: true,
|
||||||
|
});
|
||||||
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotFeedbackModal
|
||||||
|
explorer={new Explorer()}
|
||||||
|
databaseId="CopilotUserDb"
|
||||||
|
containerId="CopilotUserContainer"
|
||||||
|
mode="User"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
const dontShowAgain = wrapper.find(Checkbox);
|
const dontShowAgain = wrapper.find(Checkbox);
|
||||||
dontShowAgain.simulate("change", {}, true);
|
dontShowAgain.simulate("change", {}, true);
|
||||||
|
@ -92,7 +159,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should cancel submission", () => {
|
it("should cancel submission", () => {
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotFeedbackModal
|
||||||
|
explorer={new Explorer()}
|
||||||
|
databaseId="CopilotUserDb"
|
||||||
|
containerId="CopilotUserContainer"
|
||||||
|
mode="User"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
const cancelButton = wrapper.find(DefaultButton);
|
const cancelButton = wrapper.find(DefaultButton);
|
||||||
cancelButton.simulate("click");
|
cancelButton.simulate("click");
|
||||||
|
@ -104,7 +178,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
|
|
||||||
it("should not submit submission if required description field is null", () => {
|
it("should not submit submission if required description field is null", () => {
|
||||||
const explorer = new Explorer();
|
const explorer = new Explorer();
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotFeedbackModal
|
||||||
|
explorer={explorer}
|
||||||
|
databaseId="CopilotUserDb"
|
||||||
|
containerId="CopilotUserContainer"
|
||||||
|
mode="User"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
const submitButton = wrapper.find(PrimaryButton);
|
const submitButton = wrapper.find(PrimaryButton);
|
||||||
submitButton.simulate("click");
|
submitButton.simulate("click");
|
||||||
|
@ -114,9 +195,15 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should submit submission", () => {
|
it("should submit submission", () => {
|
||||||
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
|
|
||||||
const explorer = new Explorer();
|
const explorer = new Explorer();
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
|
const wrapper = shallow(
|
||||||
|
<QueryCopilotFeedbackModal
|
||||||
|
explorer={explorer}
|
||||||
|
databaseId="CopilotUserDb"
|
||||||
|
containerId="CopilotUserContainer"
|
||||||
|
mode="User"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
const submitButton = wrapper.find("form");
|
const submitButton = wrapper.find("form");
|
||||||
submitButton.simulate("submit");
|
submitButton.simulate("submit");
|
||||||
|
@ -124,6 +211,9 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
|
|
||||||
expect(SubmitFeedback).toHaveBeenCalledTimes(1);
|
expect(SubmitFeedback).toHaveBeenCalledTimes(1);
|
||||||
expect(SubmitFeedback).toHaveBeenCalledWith({
|
expect(SubmitFeedback).toHaveBeenCalledWith({
|
||||||
|
containerId: "CopilotUserContainer",
|
||||||
|
databaseId: "CopilotUserDb",
|
||||||
|
mode: "User",
|
||||||
params: {
|
params: {
|
||||||
likeQuery: false,
|
likeQuery: false,
|
||||||
generatedQuery: "test query",
|
generatedQuery: "test query",
|
||||||
|
|
|
@ -11,12 +11,22 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { getUserEmail } from "../../../Utils/UserUtils";
|
import { getUserEmail } from "../../../Utils/UserUtils";
|
||||||
|
|
||||||
export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }): JSX.Element => {
|
export const QueryCopilotFeedbackModal = ({
|
||||||
|
explorer,
|
||||||
|
databaseId,
|
||||||
|
containerId,
|
||||||
|
mode,
|
||||||
|
}: {
|
||||||
|
explorer: Explorer;
|
||||||
|
databaseId: string;
|
||||||
|
containerId: string;
|
||||||
|
mode: string;
|
||||||
|
}): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
generatedQuery,
|
generatedQuery,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
|
@ -24,7 +34,7 @@ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }):
|
||||||
showFeedbackModal,
|
showFeedbackModal,
|
||||||
closeFeedbackModal,
|
closeFeedbackModal,
|
||||||
setHideFeedbackModalForLikedQueries,
|
setHideFeedbackModalForLikedQueries,
|
||||||
} = useQueryCopilot();
|
} = useCopilotStore();
|
||||||
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(false);
|
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(false);
|
||||||
const [description, setDescription] = React.useState<string>("");
|
const [description, setDescription] = React.useState<string>("");
|
||||||
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
||||||
|
@ -35,7 +45,10 @@ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }):
|
||||||
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
||||||
SubmitFeedback({
|
SubmitFeedback({
|
||||||
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
||||||
explorer: explorer,
|
explorer,
|
||||||
|
databaseId,
|
||||||
|
containerId,
|
||||||
|
mode: mode,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { IconButton, Image, Link, Modal, PrimaryButton, Stack, StackItem, Text } from "@fluentui/react";
|
import { IconButton, Image, Link, Modal, PrimaryButton, Stack, StackItem, Text } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Database from "../../../../images/CopilotDatabase.svg";
|
|
||||||
import Flash from "../../../../images/CopilotFlash.svg";
|
import Flash from "../../../../images/CopilotFlash.svg";
|
||||||
import Thumb from "../../../../images/CopilotThumb.svg";
|
import Thumb from "../../../../images/CopilotThumb.svg";
|
||||||
import CoplilotWelcomeIllustration from "../../../../images/CopliotWelcomeIllustration.svg";
|
import CoplilotWelcomeIllustration from "../../../../images/CopliotWelcomeIllustration.svg";
|
||||||
|
@ -23,8 +22,10 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||||
onDismiss={hideModal}
|
onDismiss={hideModal}
|
||||||
isBlocking={false}
|
isBlocking={false}
|
||||||
styles={{
|
styles={{
|
||||||
scrollableContent: {
|
main: {
|
||||||
minHeight: 680,
|
maxHeight: 530,
|
||||||
|
borderRadius: 10,
|
||||||
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -52,7 +53,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontalAlign="center">
|
<Stack horizontalAlign="center">
|
||||||
<Stack.Item align="center" style={{ textAlign: "center" }}>
|
<Stack.Item align="center" style={{ textAlign: "center" }}>
|
||||||
<Text className="title bold">Welcome to Copilot in Azure Cosmos DB (Private Preview)</Text>
|
<Text className="title bold">Welcome to Copilot in Azure Cosmos DB</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item align="center" className="text">
|
<Stack.Item align="center" className="text">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
|
@ -69,7 +70,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||||
<Text>
|
<Text>
|
||||||
Ask Copilot to generate a query by describing the query in your words.
|
Ask Copilot to generate a query by describing the query in your words.
|
||||||
<br />
|
<br />
|
||||||
<Link target="_blank" href="https://aka.ms/cdb-copilot-learn-more">
|
<Link target="_blank" href="https://aka.ms/CopilotInAzureCDBDocs">
|
||||||
Learn more
|
Learn more
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -87,31 +88,11 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text>
|
<Text>
|
||||||
AI-generated content can have mistakes. Make sure it’s accurate and appropriate before using it.
|
AI-generated content can have mistakes. Make sure it is accurate and appropriate before executing the
|
||||||
|
query.
|
||||||
<br />
|
<br />
|
||||||
<Link target="_blank" href="https://aka.ms/cdb-copilot-preview-terms">
|
<Link target="_blank" href="https://aka.ms/cdb-copilot-preview-terms">
|
||||||
Read preview terms
|
Read our preview terms here
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item align="center" className="text">
|
|
||||||
<Stack horizontal>
|
|
||||||
<StackItem align="start" className="imageTextPadding">
|
|
||||||
<Image src={Database} />
|
|
||||||
</StackItem>
|
|
||||||
<StackItem align="start">
|
|
||||||
<Text className="bold">
|
|
||||||
Query Copilot works on a sample database.
|
|
||||||
<br />
|
|
||||||
</Text>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
<Text>
|
|
||||||
While in Private Preview, Query Copilot is setup to work on 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>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|
|
@ -291,21 +291,6 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
|
||||||
|
|
||||||
for more information.
|
for more information.
|
||||||
</Text>
|
</Text>
|
||||||
<StyledCheckboxBase
|
|
||||||
checked={false}
|
|
||||||
label="Don't show me this next time"
|
|
||||||
onChange={[Function]}
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"label": Object {
|
|
||||||
"paddingLeft": 0,
|
|
||||||
},
|
|
||||||
"root": Object {
|
|
||||||
"marginBottom": 14,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
horizontalAlign="end"
|
horizontalAlign="end"
|
||||||
|
|
|
@ -8,8 +8,10 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||||
onDismiss={[Function]}
|
onDismiss={[Function]}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"scrollableContent": Object {
|
"main": Object {
|
||||||
"minHeight": 680,
|
"borderRadius": 10,
|
||||||
|
"maxHeight": 530,
|
||||||
|
"overflow": "hidden",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +78,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||||
<Text
|
<Text
|
||||||
className="title bold"
|
className="title bold"
|
||||||
>
|
>
|
||||||
Welcome to Copilot in Azure Cosmos DB (Private Preview)
|
Welcome to Copilot in Azure Cosmos DB
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem
|
<StackItem
|
||||||
|
@ -109,7 +111,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||||
Ask Copilot to generate a query by describing the query in your words.
|
Ask Copilot to generate a query by describing the query in your words.
|
||||||
<br />
|
<br />
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://aka.ms/cdb-copilot-learn-more"
|
href="https://aka.ms/CopilotInAzureCDBDocs"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
|
@ -143,50 +145,13 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text>
|
<Text>
|
||||||
AI-generated content can have mistakes. Make sure it’s accurate and appropriate before using it.
|
AI-generated content can have mistakes. Make sure it is accurate and appropriate before executing the query.
|
||||||
<br />
|
<br />
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://aka.ms/cdb-copilot-preview-terms"
|
href="https://aka.ms/cdb-copilot-preview-terms"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Read preview terms
|
Read our preview terms here
|
||||||
</StyledLinkBase>
|
|
||||||
</Text>
|
|
||||||
</StackItem>
|
|
||||||
<StackItem
|
|
||||||
align="center"
|
|
||||||
className="text"
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
>
|
|
||||||
<StackItem
|
|
||||||
align="start"
|
|
||||||
className="imageTextPadding"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={Object {}}
|
|
||||||
/>
|
|
||||||
</StackItem>
|
|
||||||
<StackItem
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
className="bold"
|
|
||||||
>
|
|
||||||
Query Copilot works on a sample database.
|
|
||||||
<br />
|
|
||||||
</Text>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
<Text>
|
|
||||||
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you at no cost.
|
|
||||||
<br />
|
|
||||||
<StyledLinkBase
|
|
||||||
href="https://aka.ms/cdb-copilot-learn-more"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</StyledLinkBase>
|
</StyledLinkBase>
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||||
|
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,
|
||||||
|
errorMessage: "",
|
||||||
|
isSamplePromptsOpen: false,
|
||||||
|
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 }),
|
||||||
|
setErrorMessage: (errorMessage: string) => set({ errorMessage }),
|
||||||
|
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
|
||||||
|
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,
|
||||||
|
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 };
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable no-console */
|
||||||
import {
|
import {
|
||||||
Callout,
|
Callout,
|
||||||
CommandBarButton,
|
CommandBarButton,
|
||||||
|
@ -17,20 +19,20 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import {
|
|
||||||
ContainerStatusType,
|
|
||||||
PoolIdType,
|
|
||||||
QueryCopilotSampleContainerSchema,
|
|
||||||
ShortenedQueryCopilotSampleContainerSchema,
|
|
||||||
} from "Common/Constants";
|
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
||||||
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
||||||
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
||||||
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import {
|
||||||
|
SuggestedPrompt,
|
||||||
|
getSampleDatabaseSuggestedPrompts,
|
||||||
|
getSuggestedPrompts,
|
||||||
|
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
|
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
|
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
|
||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
|
@ -38,17 +40,17 @@ import HintIcon from "../../../images/Hint.svg";
|
||||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||||
import RecentIcon from "../../../images/Recent.svg";
|
import RecentIcon from "../../../images/Recent.svg";
|
||||||
import errorIcon from "../../../images/close-black.svg";
|
import errorIcon from "../../../images/close-black.svg";
|
||||||
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
|
import { useCopilotStore } from "../QueryCopilot/QueryCopilotContext";
|
||||||
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
|
||||||
type QueryCopilotPromptProps = QueryCopilotProps & {
|
type QueryCopilotPromptProps = QueryCopilotProps & {
|
||||||
|
databaseId: string;
|
||||||
|
containerId: string;
|
||||||
toggleCopilot: (toggle: boolean) => void;
|
toggleCopilot: (toggle: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SuggestedPrompt {
|
|
||||||
id: number;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const promptStyles: IButtonStyles = {
|
const promptStyles: IButtonStyles = {
|
||||||
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
|
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
|
||||||
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
||||||
|
@ -57,10 +59,13 @@ const promptStyles: IButtonStyles = {
|
||||||
export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
explorer,
|
explorer,
|
||||||
toggleCopilot,
|
toggleCopilot,
|
||||||
|
databaseId,
|
||||||
|
containerId,
|
||||||
}: QueryCopilotPromptProps): JSX.Element => {
|
}: QueryCopilotPromptProps): JSX.Element => {
|
||||||
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
||||||
const inputEdited = useRef(false);
|
const inputEdited = useRef(false);
|
||||||
const {
|
const {
|
||||||
|
openFeedbackModal,
|
||||||
hideFeedbackModalForLikedQueries,
|
hideFeedbackModalForLikedQueries,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
setUserPrompt,
|
setUserPrompt,
|
||||||
|
@ -93,7 +98,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
setGeneratedQueryComments,
|
setGeneratedQueryComments,
|
||||||
setQueryResults,
|
setQueryResults,
|
||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
} = useQueryCopilot();
|
} = useCopilotStore();
|
||||||
|
|
||||||
const sampleProps: SamplePromptsProps = {
|
const sampleProps: SamplePromptsProps = {
|
||||||
isSamplePromptsOpen: isSamplePromptsOpen,
|
isSamplePromptsOpen: isSamplePromptsOpen,
|
||||||
|
@ -118,14 +123,13 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
}, 6000);
|
}, 6000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||||
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
||||||
const cachedHistories = cachedHistoriesString?.split("|");
|
const cachedHistories = cachedHistoriesString?.split("|");
|
||||||
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
||||||
const suggestedPrompts: SuggestedPrompt[] = [
|
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
|
||||||
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
|
? getSampleDatabaseSuggestedPrompts()
|
||||||
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
|
: getSuggestedPrompts();
|
||||||
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
|
|
||||||
];
|
|
||||||
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
|
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
|
||||||
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
|
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
|
||||||
|
|
||||||
|
@ -176,16 +180,11 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
setShowDeletePopup(false);
|
setShowDeletePopup(false);
|
||||||
useTabs.getState().setIsTabExecuting(true);
|
useTabs.getState().setIsTabExecuting(true);
|
||||||
useTabs.getState().setIsQueryErrorThrown(false);
|
useTabs.getState().setIsQueryErrorThrown(false);
|
||||||
if (
|
const mode: string = isSampleCopilotActive ? "Sample" : "User";
|
||||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
|
||||||
!userContext.features.disableCopilotPhoenixGateaway
|
await allocatePhoenixContainer({ explorer, databaseId, containerId, mode });
|
||||||
) {
|
|
||||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
|
||||||
}
|
|
||||||
const payload = {
|
const payload = {
|
||||||
containerSchema: userContext.features.enableCopilotFullSchema
|
|
||||||
? QueryCopilotSampleContainerSchema
|
|
||||||
: ShortenedQueryCopilotSampleContainerSchema,
|
|
||||||
userPrompt: userPrompt,
|
userPrompt: userPrompt,
|
||||||
};
|
};
|
||||||
useQueryCopilot.getState().refreshCorrelationId();
|
useQueryCopilot.getState().refreshCorrelationId();
|
||||||
|
@ -198,6 +197,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||||
|
Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
@ -215,13 +215,30 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||||
setShowFeedbackBar(true);
|
setShowFeedbackBar(true);
|
||||||
resetQueryResults();
|
resetQueryResults();
|
||||||
|
TelemetryProcessor.traceSuccess(Action.QueryGenerationFromCopilotPrompt, {
|
||||||
|
databaseName: databaseId,
|
||||||
|
collectionId: containerId,
|
||||||
|
copilotLatency:
|
||||||
|
Date.parse(generateSQLQueryResponse?.generateEnd) - Date.parse(generateSQLQueryResponse?.generateStart),
|
||||||
|
responseCode: response.status,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setShowInvalidQueryMessageBar(true);
|
setShowInvalidQueryMessageBar(true);
|
||||||
|
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
|
||||||
|
databaseName: databaseId,
|
||||||
|
collectionId: containerId,
|
||||||
|
responseCode: response.status,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
|
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
|
||||||
useTabs.getState().setIsQueryErrorThrown(true);
|
useTabs.getState().setIsQueryErrorThrown(true);
|
||||||
setShowErrorMessageBar(true);
|
setShowErrorMessageBar(true);
|
||||||
|
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
|
||||||
|
databaseName: databaseId,
|
||||||
|
collectionId: containerId,
|
||||||
|
responseCode: response.status,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "executeNaturalLanguageQuery");
|
handleError(error, "executeNaturalLanguageQuery");
|
||||||
|
@ -310,6 +327,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }}
|
styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }}
|
||||||
disabled={isGeneratingQuery}
|
disabled={isGeneratingQuery}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||||
/>
|
/>
|
||||||
{copilotTeachingBubbleVisible && (
|
{copilotTeachingBubbleVisible && (
|
||||||
<TeachingBubble
|
<TeachingBubble
|
||||||
|
@ -489,7 +507,10 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
description: "",
|
description: "",
|
||||||
userPrompt: userPrompt,
|
userPrompt: userPrompt,
|
||||||
},
|
},
|
||||||
explorer: explorer,
|
explorer,
|
||||||
|
databaseId,
|
||||||
|
containerId,
|
||||||
|
mode: isSampleCopilotActive ? "Sample" : "User",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
directionalHint={DirectionalHint.topCenter}
|
directionalHint={DirectionalHint.topCenter}
|
||||||
|
@ -499,7 +520,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
<Link
|
<Link
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCallout(false);
|
setShowCallout(false);
|
||||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
openFeedbackModal(generatedQuery, true, userPrompt);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
more feedback?
|
more feedback?
|
||||||
|
@ -524,7 +545,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!dislikeQuery) {
|
if (!dislikeQuery) {
|
||||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
openFeedbackModal(generatedQuery, false, userPrompt);
|
||||||
setLikeQuery(false);
|
setLikeQuery(false);
|
||||||
}
|
}
|
||||||
setDislikeQuery(!dislikeQuery);
|
setDislikeQuery(!dislikeQuery);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { Stack } from "@fluentui/react";
|
import { Stack } from "@fluentui/react";
|
||||||
|
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
@ -11,6 +12,7 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import SplitterLayout from "react-splitter-layout";
|
import SplitterLayout from "react-splitter-layout";
|
||||||
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
||||||
|
@ -28,13 +30,14 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||||
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
||||||
: true;
|
: true;
|
||||||
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
|
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
|
||||||
|
const [tabActive, setTabActive] = useState<boolean>(true);
|
||||||
|
|
||||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||||
const executeQueryBtn = {
|
const executeQueryBtn = {
|
||||||
iconSrc: ExecuteQueryIcon,
|
iconSrc: ExecuteQueryIcon,
|
||||||
iconAlt: executeQueryBtnLabel,
|
iconAlt: executeQueryBtnLabel,
|
||||||
onCommandClick: () => OnExecuteQueryClick(),
|
onCommandClick: () => OnExecuteQueryClick(useQueryCopilot),
|
||||||
commandButtonLabel: executeQueryBtnLabel,
|
commandButtonLabel: executeQueryBtnLabel,
|
||||||
ariaLabel: executeQueryBtnLabel,
|
ariaLabel: executeQueryBtnLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
@ -73,10 +76,13 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
const commandbarButtons = getCommandbarButtons();
|
useTabs.subscribe((state: TabsState) => {
|
||||||
commandbarButtons.pop();
|
if (state.activeReactTab === ReactTabKind.QueryCopilot) {
|
||||||
commandbarButtons.map((props: CommandButtonComponentProps) => (props.disabled = true));
|
setTabActive(true);
|
||||||
useCommandBar.getState().setContextButtons(commandbarButtons);
|
} else {
|
||||||
|
setTabActive(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -88,8 +94,13 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||||
return (
|
return (
|
||||||
<Stack className="tab-pane" style={{ width: "100%" }}>
|
<Stack className="tab-pane" style={{ width: "100%" }}>
|
||||||
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
||||||
{copilotActive && (
|
{tabActive && copilotActive && (
|
||||||
<QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar>
|
<QueryCopilotPromptbar
|
||||||
|
explorer={explorer}
|
||||||
|
toggleCopilot={toggleCopilot}
|
||||||
|
databaseId={QueryCopilotSampleDatabaseId}
|
||||||
|
containerId={QueryCopilotSampleContainerId}
|
||||||
|
></QueryCopilotPromptbar>
|
||||||
)}
|
)}
|
||||||
<Stack className="tabPaneContentContainer">
|
<Stack className="tabPaneContentContainer">
|
||||||
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
||||||
|
|
|
@ -7,6 +7,11 @@ import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
||||||
import DocumentId from "Explorer/Tree/DocumentId";
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
|
|
||||||
|
export interface SuggestedPrompt {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
|
export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
|
||||||
options = getCommonQueryOptions(options);
|
options = getCommonQueryOptions(options);
|
||||||
return sampleDataClient()
|
return sampleDataClient()
|
||||||
|
@ -33,3 +38,19 @@ export const readSampleDocument = async (documentId: DocumentId): Promise<Item>
|
||||||
clearMessage();
|
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" },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema } from "Common/Constants";
|
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
|
@ -37,9 +36,6 @@ describe("Query Copilot Client", () => {
|
||||||
userPrompt: "UserPrompt",
|
userPrompt: "UserPrompt",
|
||||||
description: "Description",
|
description: "Description",
|
||||||
contact: "Contact",
|
contact: "Contact",
|
||||||
containerSchema: userContext.features.enableCopilotFullSchema
|
|
||||||
? QueryCopilotSampleContainerSchema
|
|
||||||
: ShortenedQueryCopilotSampleContainerSchema,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockStore = useQueryCopilot.getState();
|
const mockStore = useQueryCopilot.getState();
|
||||||
|
@ -59,6 +55,9 @@ describe("Query Copilot Client", () => {
|
||||||
|
|
||||||
globalThis.fetch = mockFetch;
|
globalThis.fetch = mockFetch;
|
||||||
await SubmitFeedback({
|
await SubmitFeedback({
|
||||||
|
databaseId: "test",
|
||||||
|
containerId: "test",
|
||||||
|
mode: "User",
|
||||||
params: {
|
params: {
|
||||||
likeQuery: true,
|
likeQuery: true,
|
||||||
generatedQuery: "GeneratedQuery",
|
generatedQuery: "GeneratedQuery",
|
||||||
|
@ -91,6 +90,9 @@ describe("Query Copilot Client", () => {
|
||||||
globalThis.fetch = mockFetch;
|
globalThis.fetch = mockFetch;
|
||||||
|
|
||||||
await SubmitFeedback({
|
await SubmitFeedback({
|
||||||
|
databaseId: "test",
|
||||||
|
containerId: "test",
|
||||||
|
mode: "User",
|
||||||
params: {
|
params: {
|
||||||
likeQuery: false,
|
likeQuery: false,
|
||||||
generatedQuery: "GeneratedQuery",
|
generatedQuery: "GeneratedQuery",
|
||||||
|
@ -108,6 +110,7 @@ describe("Query Copilot Client", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"x-ms-correlationid": "mocked-correlation-id",
|
"x-ms-correlationid": "mocked-correlation-id",
|
||||||
|
Authorization: "token mocked-token",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -120,6 +123,9 @@ describe("Query Copilot Client", () => {
|
||||||
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
|
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
|
||||||
|
|
||||||
await SubmitFeedback({
|
await SubmitFeedback({
|
||||||
|
databaseId: "test",
|
||||||
|
containerId: "test",
|
||||||
|
mode: "User",
|
||||||
params: {
|
params: {
|
||||||
likeQuery: true,
|
likeQuery: true,
|
||||||
generatedQuery: "GeneratedQuery",
|
generatedQuery: "GeneratedQuery",
|
||||||
|
|
|
@ -1,28 +1,174 @@
|
||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
|
Areas,
|
||||||
|
ConnectionStatusType,
|
||||||
ContainerStatusType,
|
ContainerStatusType,
|
||||||
|
HttpStatusCodes,
|
||||||
PoolIdType,
|
PoolIdType,
|
||||||
QueryCopilotSampleContainerId,
|
QueryCopilotSampleContainerId,
|
||||||
QueryCopilotSampleContainerSchema,
|
QueryCopilotSampleContainerSchema,
|
||||||
ShortenedQueryCopilotSampleContainerSchema,
|
ShortenedQueryCopilotSampleContainerSchema,
|
||||||
} from "Common/Constants";
|
} from "Common/Constants";
|
||||||
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack, handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
||||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
||||||
import { QueryResults } from "Contracts/ViewModels";
|
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 Explorer from "Explorer/Explorer";
|
||||||
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
import * as StringUtility from "../../../Shared/StringUtility";
|
import * as StringUtility from "../../../Shared/StringUtility";
|
||||||
|
|
||||||
|
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/CopilotInAzureCDB?api-version=${api_version}`;
|
||||||
|
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||||
|
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await window.fetch(url, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response?.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureRegistration = (await response?.json()) as FeatureRegistration;
|
||||||
|
return featureRegistration?.properties?.state === "Registered";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||||
|
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
|
||||||
|
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||||
|
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await window.fetch(url, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response?.ok) {
|
||||||
|
throw new Error(await response?.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
const copilotPortalConfiguration = (await response?.json()) as CopilotEnabledConfiguration;
|
||||||
|
return copilotPortalConfiguration?.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 ({
|
export const SendQueryRequest = async ({
|
||||||
userPrompt,
|
userPrompt,
|
||||||
explorer,
|
explorer,
|
||||||
|
@ -106,16 +252,19 @@ export const SendQueryRequest = async ({
|
||||||
export const SubmitFeedback = async ({
|
export const SubmitFeedback = async ({
|
||||||
params,
|
params,
|
||||||
explorer,
|
explorer,
|
||||||
|
databaseId,
|
||||||
|
containerId,
|
||||||
|
mode,
|
||||||
}: {
|
}: {
|
||||||
params: FeedbackParams;
|
params: FeedbackParams;
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
|
databaseId: string;
|
||||||
|
containerId: string;
|
||||||
|
mode: string;
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
|
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
|
||||||
const payload = {
|
const payload = {
|
||||||
containerSchema: userContext.features.enableCopilotFullSchema
|
|
||||||
? QueryCopilotSampleContainerSchema
|
|
||||||
: ShortenedQueryCopilotSampleContainerSchema,
|
|
||||||
like: likeQuery ? "like" : "dislike",
|
like: likeQuery ? "like" : "dislike",
|
||||||
generatedSql: generatedQuery,
|
generatedSql: generatedQuery,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
|
@ -126,7 +275,7 @@ export const SubmitFeedback = async ({
|
||||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
||||||
!userContext.features.disableCopilotPhoenixGateaway
|
!userContext.features.disableCopilotPhoenixGateaway
|
||||||
) {
|
) {
|
||||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
await allocatePhoenixContainer({ explorer, databaseId, containerId, mode });
|
||||||
}
|
}
|
||||||
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
|
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
|
||||||
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
|
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
|
||||||
|
@ -137,6 +286,7 @@ export const SubmitFeedback = async ({
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||||
|
Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
@ -145,7 +295,7 @@ export const SubmitFeedback = async ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OnExecuteQueryClick = async (): Promise<void> => {
|
export const OnExecuteQueryClick = async (useQueryCopilot: Partial<QueryCopilotState>): Promise<void> => {
|
||||||
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
correlationId: useQueryCopilot.getState().correlationId,
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
userPrompt: useQueryCopilot.getState().userPrompt,
|
userPrompt: useQueryCopilot.getState().userPrompt,
|
||||||
|
@ -160,13 +310,14 @@ export const OnExecuteQueryClick = async (): Promise<void> => {
|
||||||
useQueryCopilot.getState().setQueryIterator(queryIterator);
|
useQueryCopilot.getState().setQueryIterator(queryIterator);
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await QueryDocumentsPerPage(0, queryIterator);
|
await QueryDocumentsPerPage(0, queryIterator, useQueryCopilot);
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueryDocumentsPerPage = async (
|
export const QueryDocumentsPerPage = async (
|
||||||
firstItemIndex: number,
|
firstItemIndex: number,
|
||||||
queryIterator: MinimalQueryIterator,
|
queryIterator: MinimalQueryIterator,
|
||||||
|
useQueryCopilot: Partial<QueryCopilotState>,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
useQueryCopilot.getState().setIsExecuting(true);
|
useQueryCopilot.getState().setIsExecuting(true);
|
||||||
|
|
|
@ -32,3 +32,8 @@ export interface FeedbackParams {
|
||||||
export interface QueryCopilotProps {
|
export interface QueryCopilotProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CopilotSchemaAllocationInfo {
|
||||||
|
databaseId: string;
|
||||||
|
containerId: string;
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const QueryCopilotResults: React.FC = (): JSX.Element => {
|
||||||
queryResults={useQueryCopilot.getState().queryResults}
|
queryResults={useQueryCopilot.getState().queryResults}
|
||||||
isExecuting={useQueryCopilot.getState().isExecuting}
|
isExecuting={useQueryCopilot.getState().isExecuting}
|
||||||
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||||
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator)
|
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator, useQueryCopilot)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,6 +18,8 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<QueryCopilotPromptbar
|
<QueryCopilotPromptbar
|
||||||
|
containerId="SampleContainer"
|
||||||
|
databaseId="CopilotSampleDb"
|
||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { useCarousel } from "hooks/useCarousel";
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
import { usePostgres } from "hooks/usePostgres";
|
import { usePostgres } from "hooks/usePostgres";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||||
|
@ -104,6 +105,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
(state) => state.sampleDataResourceTokenCollection,
|
(state) => state.sampleDataResourceTokenCollection,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
dispose: useQueryCopilot.subscribe(
|
||||||
|
() => this.setState({}),
|
||||||
|
(state) => state.copilotEnabled,
|
||||||
|
),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,9 +121,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||||
|
|
||||||
private getSplashScreenButtons = (): JSX.Element => {
|
private getSplashScreenButtons = (): JSX.Element => {
|
||||||
if (
|
if (
|
||||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
userContext.apiType === "SQL" &&
|
||||||
userContext.features.enableCopilot &&
|
useQueryCopilot.getState().copilotEnabled &&
|
||||||
userContext.apiType === "SQL"
|
useDatabases.getState().sampleDataResourceTokenCollection
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
|
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
|
import { QueryConstants } from "Shared/Constants";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import { format } from "react-string-format";
|
import { format } from "react-string-format";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
|
||||||
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
||||||
import NewDocumentIcon from "../../../images/NewDocument.svg";
|
import NewDocumentIcon from "../../../images/NewDocument.svg";
|
||||||
import UploadIcon from "../../../images/Upload_16x16.svg";
|
import UploadIcon from "../../../images/Upload_16x16.svg";
|
||||||
|
@ -331,6 +331,7 @@ export default class DocumentsTab extends TabsBase {
|
||||||
|
|
||||||
this.showPartitionKey = this._shouldShowPartitionKey();
|
this.showPartitionKey = this._shouldShowPartitionKey();
|
||||||
this._isQueryCopilotSampleContainer =
|
this._isQueryCopilotSampleContainer =
|
||||||
|
this.collection?.isSampleCollection &&
|
||||||
this.collection?.databaseId === QueryCopilotSampleDatabaseId &&
|
this.collection?.databaseId === QueryCopilotSampleDatabaseId &&
|
||||||
this.collection?.id() === QueryCopilotSampleContainerId;
|
this.collection?.id() === QueryCopilotSampleContainerId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
|
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
|
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
|
||||||
import { useTabs } from "../../../hooks/useTabs";
|
import { useTabs } from "../../../hooks/useTabs";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent";
|
import QueryTabComponent, {
|
||||||
|
IQueryTabComponentProps,
|
||||||
|
ITabAccessor,
|
||||||
|
QueryTabFunctionComponent,
|
||||||
|
} from "../../Tabs/QueryTab/QueryTabComponent";
|
||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
import QueryTabComponent from "./QueryTabComponent";
|
|
||||||
|
|
||||||
export interface IQueryTabProps {
|
export interface IQueryTabProps {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
|
@ -40,7 +45,13 @@ export class NewQueryTab extends TabsBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return <QueryTabComponent {...this.iQueryTabComponentProps} />;
|
return userContext.apiType === "SQL" ? (
|
||||||
|
<CopilotProvider>
|
||||||
|
<QueryTabFunctionComponent {...this.iQueryTabComponentProps} />
|
||||||
|
</CopilotProvider>
|
||||||
|
) : (
|
||||||
|
<QueryTabComponent {...this.iQueryTabComponentProps} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onTabClick(): void {
|
public onTabClick(): void {
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
import { fireEvent, render } from "@testing-library/react";
|
import { fireEvent, render } from "@testing-library/react";
|
||||||
import QueryTabComponent, { IQueryTabComponentProps } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
|
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
|
import QueryTabComponent, {
|
||||||
|
IQueryTabComponentProps,
|
||||||
|
QueryTabFunctionComponent,
|
||||||
|
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
|
import { updateUserContext, userContext } from "UserContext";
|
||||||
|
import { mount } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact");
|
jest.mock("Explorer/Controls/Editor/EditorReact");
|
||||||
|
@ -11,9 +21,15 @@ describe("QueryTabComponent", () => {
|
||||||
mockStore.showCopilotSidebar = false;
|
mockStore.showCopilotSidebar = false;
|
||||||
mockStore.setShowCopilotSidebar = jest.fn();
|
mockStore.setShowCopilotSidebar = jest.fn();
|
||||||
});
|
});
|
||||||
beforeEach(() => jest.clearAllMocks());
|
afterEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
it("should launch Copilot when ALT+C is pressed", () => {
|
it("should launch conversational Copilot when ALT+C is pressed and when copilot version is 3", () => {
|
||||||
|
updateUserContext({
|
||||||
|
features: {
|
||||||
|
...userContext.features,
|
||||||
|
copilotVersion: "v3.0",
|
||||||
|
},
|
||||||
|
});
|
||||||
const propsMock: Readonly<IQueryTabComponentProps> = {
|
const propsMock: Readonly<IQueryTabComponentProps> = {
|
||||||
collection: { databaseId: "CopilotSampleDb" },
|
collection: { databaseId: "CopilotSampleDb" },
|
||||||
onTabAccessor: () => jest.fn(),
|
onTabAccessor: () => jest.fn(),
|
||||||
|
@ -31,4 +47,32 @@ describe("QueryTabComponent", () => {
|
||||||
|
|
||||||
expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true);
|
expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("copilot should be enabled by default when tab is active", () => {
|
||||||
|
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>
|
||||||
|
<QueryTabFunctionComponent {...propsMock} />
|
||||||
|
</CopilotProvider>,
|
||||||
|
);
|
||||||
|
expect(container.find(QueryCopilotPromptbar).exists()).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,21 +1,29 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable no-console */
|
||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
|
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||||
|
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import { TabsState, useTabs } from "hooks/useTabs";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import SplitterLayout from "react-splitter-layout";
|
import SplitterLayout from "react-splitter-layout";
|
||||||
import "react-splitter-layout/lib/index.css";
|
import "react-splitter-layout/lib/index.css";
|
||||||
import { format } from "react-string-format";
|
import { format } from "react-string-format";
|
||||||
|
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
||||||
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||||
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
|
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
|
||||||
import { NormalizedEventKey, QueryCopilotSampleDatabaseId } from "../../../Common/Constants";
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||||
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
||||||
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
|
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
|
||||||
|
@ -24,6 +32,8 @@ import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import * as StringUtility from "../../../Shared/StringUtility";
|
||||||
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
|
@ -72,6 +82,9 @@ export interface IQueryTabComponentProps {
|
||||||
isPreferredApiMongoDB?: boolean;
|
isPreferredApiMongoDB?: boolean;
|
||||||
monacoEditorSetting?: string;
|
monacoEditorSetting?: string;
|
||||||
viewModelcollection?: ViewModels.Collection;
|
viewModelcollection?: ViewModels.Collection;
|
||||||
|
copilotEnabled?: boolean;
|
||||||
|
isSampleCopilotActive?: boolean;
|
||||||
|
copilotStore?: Partial<QueryCopilotState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IQueryTabStates {
|
interface IQueryTabStates {
|
||||||
|
@ -85,8 +98,24 @@ interface IQueryTabStates {
|
||||||
showCopilotSidebar: boolean;
|
showCopilotSidebar: boolean;
|
||||||
queryCopilotGeneratedQuery: string;
|
queryCopilotGeneratedQuery: string;
|
||||||
cancelQueryTimeoutID: NodeJS.Timeout;
|
cancelQueryTimeoutID: NodeJS.Timeout;
|
||||||
|
copilotActive: boolean;
|
||||||
|
currentTabActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const QueryTabFunctionComponent = (props: IQueryTabComponentProps): any => {
|
||||||
|
const copilotStore = useCopilotStore();
|
||||||
|
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||||
|
const queryTabProps = {
|
||||||
|
...props,
|
||||||
|
copilotEnabled:
|
||||||
|
useQueryCopilot().copilotEnabled &&
|
||||||
|
(useQueryCopilot().copilotUserDBEnabled || (isSampleCopilotActive && !!userContext.sampleDataConnectionInfo)),
|
||||||
|
isSampleCopilotActive: isSampleCopilotActive,
|
||||||
|
copilotStore: copilotStore,
|
||||||
|
};
|
||||||
|
return <QueryTabComponent {...queryTabProps}></QueryTabComponent>;
|
||||||
|
};
|
||||||
|
|
||||||
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
||||||
public queryEditorId: string;
|
public queryEditorId: string;
|
||||||
public executeQueryButton: Button;
|
public executeQueryButton: Button;
|
||||||
|
@ -113,12 +142,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
|
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
|
||||||
queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
|
queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
|
||||||
cancelQueryTimeoutID: undefined,
|
cancelQueryTimeoutID: undefined,
|
||||||
|
copilotActive: this._queryCopilotActive(),
|
||||||
|
currentTabActive: true,
|
||||||
};
|
};
|
||||||
this.isCloseClicked = false;
|
this.isCloseClicked = false;
|
||||||
this.splitterId = this.props.tabId + "_splitter";
|
this.splitterId = this.props.tabId + "_splitter";
|
||||||
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
||||||
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
||||||
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
|
this.isCopilotTabActive = userContext.features.copilotVersion === "v3.0";
|
||||||
this.executeQueryButton = {
|
this.executeQueryButton = {
|
||||||
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
||||||
visible: true,
|
visible: true,
|
||||||
|
@ -143,6 +174,19 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _queryCopilotActive(): boolean {
|
||||||
|
if (this.props.copilotEnabled) {
|
||||||
|
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
||||||
|
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
||||||
|
);
|
||||||
|
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
||||||
|
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
||||||
|
: true;
|
||||||
|
return copilotInitialActive;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public onCloseClick(isClicked: boolean): void {
|
public onCloseClick(isClicked: boolean): void {
|
||||||
this.isCloseClicked = isClicked;
|
this.isCloseClicked = isClicked;
|
||||||
if (useQueryCopilot.getState().wasCopilotUsed && this.isCopilotTabActive) {
|
if (useQueryCopilot.getState().wasCopilotUsed && this.isCopilotTabActive) {
|
||||||
|
@ -167,6 +211,16 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await this._executeQueryDocumentsPage(0);
|
await this._executeQueryDocumentsPage(0);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
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 onSaveQueryClick = (): void => {
|
public onSaveQueryClick = (): void => {
|
||||||
|
@ -326,7 +380,9 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: ExecuteQueryIcon,
|
iconSrc: ExecuteQueryIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: this.isCopilotTabActive ? () => OnExecuteQueryClick() : this.onExecuteQueryClick,
|
onCommandClick: this.props.isSampleCopilotActive
|
||||||
|
? () => OnExecuteQueryClick(this.props.copilotStore)
|
||||||
|
: this.onExecuteQueryClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
@ -380,6 +436,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
buttons.push(launchCopilotButton);
|
buttons.push(launchCopilotButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.copilotEnabled) {
|
||||||
|
const toggleCopilotButton = {
|
||||||
|
iconSrc: QueryCommandIcon,
|
||||||
|
iconAlt: "Copilot",
|
||||||
|
onCommandClick: () => {
|
||||||
|
this._toggleCopilot(!this.state.copilotActive);
|
||||||
|
},
|
||||||
|
commandButtonLabel: "Copilot",
|
||||||
|
ariaLabel: "Copilot",
|
||||||
|
hasPopup: false,
|
||||||
|
};
|
||||||
|
buttons.push(toggleCopilotButton);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
|
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
|
||||||
const label = "Cancel query";
|
const label = "Cancel query";
|
||||||
buttons.push({
|
buttons.push({
|
||||||
|
@ -395,11 +465,31 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _toggleCopilot = (active: boolean) => {
|
||||||
|
this.setState({ copilotActive: active });
|
||||||
|
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
|
||||||
|
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, active.toString());
|
||||||
|
|
||||||
|
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 {
|
public onChangeContent(newContent: string): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
sqlQueryEditorContent: newContent,
|
sqlQueryEditorContent: newContent,
|
||||||
queryCopilotGeneratedQuery: "",
|
queryCopilotGeneratedQuery: "",
|
||||||
});
|
});
|
||||||
|
if (this.state.copilotActive) {
|
||||||
|
this.props.copilotStore?.setQuery(newContent);
|
||||||
|
}
|
||||||
if (this.isPreferredApiMongoDB) {
|
if (this.isPreferredApiMongoDB) {
|
||||||
if (newContent.length > 0) {
|
if (newContent.length > 0) {
|
||||||
this.executeQueryButton = {
|
this.executeQueryButton = {
|
||||||
|
@ -434,6 +524,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
: useQueryCopilot.getState().setSelectedQuery("");
|
: useQueryCopilot.getState().setSelectedQuery("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.copilotActive) {
|
||||||
|
this.props.copilotStore?.setSelectedQuery(selectedContent);
|
||||||
|
}
|
||||||
|
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,6 +536,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
return this.state.queryCopilotGeneratedQuery;
|
return this.state.queryCopilotGeneratedQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.copilotActive) {
|
||||||
|
return this.props.copilotStore?.query;
|
||||||
|
}
|
||||||
|
|
||||||
return this.state.sqlQueryEditorContent;
|
return this.state.sqlQueryEditorContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,12 +550,15 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
private unsubscribeCopilotSidebar: () => void;
|
private unsubscribeCopilotSidebar: () => void;
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
this.unsubscribeCopilotSidebar = useQueryCopilot.subscribe((state: QueryCopilotState) => {
|
useTabs.subscribe((state: TabsState) => {
|
||||||
if (this.state.showCopilotSidebar !== state.showCopilotSidebar) {
|
if (this.state.currentTabActive && state.activeTab?.tabId !== this.props.tabId) {
|
||||||
this.setState({ showCopilotSidebar: state.showCopilotSidebar });
|
this.setState({
|
||||||
}
|
currentTabActive: false,
|
||||||
if (this.state.queryCopilotGeneratedQuery !== state.query) {
|
});
|
||||||
this.setState({ queryCopilotGeneratedQuery: state.query });
|
} else if (!this.state.currentTabActive && state.activeTab?.tabId === this.props.tabId) {
|
||||||
|
this.setState({
|
||||||
|
currentTabActive: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -466,7 +567,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
this.unsubscribeCopilotSidebar();
|
|
||||||
document.removeEventListener("keydown", this.handleCopilotKeyDown);
|
document.removeEventListener("keydown", this.handleCopilotKeyDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,6 +574,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="tab-pane" id={this.props.tabId} role="tabpanel">
|
<div className="tab-pane" id={this.props.tabId} role="tabpanel">
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
<div className="tabPaneContentContainer">
|
<div className="tabPaneContentContainer">
|
||||||
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -482,6 +590,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
language={"sql"}
|
language={"sql"}
|
||||||
content={this.setEditorContent()}
|
content={this.setEditorContent()}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
|
wordWrap={"on"}
|
||||||
ariaLabel={"Editing Query"}
|
ariaLabel={"Editing Query"}
|
||||||
lineNumbers={"on"}
|
lineNumbers={"on"}
|
||||||
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
|
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
|
||||||
|
@ -489,8 +598,21 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
{this.isCopilotTabActive ? (
|
{this.props.isSampleCopilotActive ? (
|
||||||
<QueryCopilotResults />
|
<QueryResultSection
|
||||||
|
isMongoDB={this.props.isPreferredApiMongoDB}
|
||||||
|
queryEditorContent={this.state.sqlQueryEditorContent}
|
||||||
|
error={this.props.copilotStore?.errorMessage}
|
||||||
|
queryResults={this.props.copilotStore?.queryResults}
|
||||||
|
isExecuting={this.props.copilotStore?.isExecuting}
|
||||||
|
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||||
|
QueryDocumentsPerPage(
|
||||||
|
firstItemIndex,
|
||||||
|
this.props.copilotStore.queryIterator,
|
||||||
|
this.props.copilotStore,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<QueryResultSection
|
<QueryResultSection
|
||||||
isMongoDB={this.props.isPreferredApiMongoDB}
|
isMongoDB={this.props.isPreferredApiMongoDB}
|
||||||
|
@ -506,6 +628,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||||
</SplitterLayout>
|
</SplitterLayout>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{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>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||||
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
||||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
|
@ -170,7 +169,7 @@ const CloseButton = ({
|
||||||
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
|
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
|
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
|
||||||
tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates();
|
// tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates();
|
||||||
}}
|
}}
|
||||||
tabIndex={active ? 0 : undefined}
|
tabIndex={active ? 0 : undefined}
|
||||||
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
|
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
|
||||||
|
@ -256,6 +255,7 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
switch (activeReactTab) {
|
switch (activeReactTab) {
|
||||||
case ReactTabKind.Connect:
|
case ReactTabKind.Connect:
|
||||||
return userContext.apiType === "VCoreMongo" ? (
|
return userContext.apiType === "VCoreMongo" ? (
|
||||||
|
|
|
@ -3,15 +3,15 @@ import * as Constants from "../../Common/Constants";
|
||||||
import * as ThemeUtility from "../../Common/ThemeUtility";
|
import * as ThemeUtility from "../../Common/ThemeUtility";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
|
||||||
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
|
||||||
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
// TODO: Use specific actions for logging telemetry data
|
// TODO: Use specific actions for logging telemetry data
|
||||||
export default class TabsBase extends WaitsForTemplateViewModel {
|
export default class TabsBase extends WaitsForTemplateViewModel {
|
||||||
private static id = 0;
|
private static id = 0;
|
||||||
|
|
|
@ -177,8 +177,8 @@ export default class Collection implements ViewModels.Collection {
|
||||||
this.children.subscribe(() => {
|
this.children.subscribe(() => {
|
||||||
// update the database in zustand store
|
// update the database in zustand store
|
||||||
const database = this.getDatabase();
|
const database = this.getDatabase();
|
||||||
database.collections(
|
database?.collections(
|
||||||
database.collections()?.map((collection) => {
|
database?.collections()?.map((collection) => {
|
||||||
if (collection.id() === this.id()) {
|
if (collection.id() === this.id()) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,6 +156,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDatabase(): ViewModels.Database {
|
public getDatabase(): ViewModels.Database {
|
||||||
return useDatabases.getState().findDatabaseWithId(this.databaseId);
|
return useDatabases.getState().findDatabaseWithId(this.databaseId, this.isSampleCollection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||||
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
|
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
|
||||||
import { getItemName } from "Utils/APITypeUtils";
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import shallow from "zustand/shallow";
|
import shallow from "zustand/shallow";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
|
@ -767,7 +768,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataRootNode = buildDataTree();
|
const dataRootNode = buildDataTree();
|
||||||
const isSampleDataEnabled = userContext.sampleDataConnectionInfo && userContext.apiType === "SQL";
|
const isSampleDataEnabled =
|
||||||
|
useQueryCopilot().copilotEnabled && userContext.sampleDataConnectionInfo && userContext.apiType === "SQL";
|
||||||
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
|
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -14,7 +14,7 @@ interface DatabasesState {
|
||||||
deleteDatabase: (database: ViewModels.Database) => void;
|
deleteDatabase: (database: ViewModels.Database) => void;
|
||||||
clearDatabases: () => void;
|
clearDatabases: () => void;
|
||||||
isSaveQueryEnabled: () => boolean;
|
isSaveQueryEnabled: () => boolean;
|
||||||
findDatabaseWithId: (databaseId: string) => ViewModels.Database;
|
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => ViewModels.Database;
|
||||||
isLastNonEmptyDatabase: () => boolean;
|
isLastNonEmptyDatabase: () => boolean;
|
||||||
findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection;
|
findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection;
|
||||||
isLastCollection: () => boolean;
|
isLastCollection: () => boolean;
|
||||||
|
@ -33,7 +33,7 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
||||||
updateDatabase: (updatedDatabase: ViewModels.Database) =>
|
updateDatabase: (updatedDatabase: ViewModels.Database) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
|
const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
|
||||||
if (database.id() === updatedDatabase.id()) {
|
if (database?.id() === updatedDatabase?.id()) {
|
||||||
return updatedDatabase;
|
return updatedDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +67,9 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
findDatabaseWithId: (databaseId: string) => get().databases.find((db) => databaseId === db.id()),
|
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => {
|
||||||
|
return get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase);
|
||||||
|
},
|
||||||
isLastNonEmptyDatabase: () => {
|
isLastNonEmptyDatabase: () => {
|
||||||
const databases = get().databases;
|
const databases = get().databases;
|
||||||
return databases.length === 1 && (databases[0].collections()?.length > 0 || !!databases[0].offer());
|
return databases.length === 1 && (databases[0].collections()?.length > 0 || !!databases[0].offer());
|
||||||
|
|
22
src/Main.tsx
22
src/Main.tsx
|
@ -1,13 +1,13 @@
|
||||||
// CSS Dependencies
|
// CSS Dependencies
|
||||||
import { initializeIcons, loadTheme } from "@fluentui/react";
|
import { initializeIcons, loadTheme } from "@fluentui/react";
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
|
||||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||||
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||||
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import { useCarousel } from "hooks/useCarousel";
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import "../externals/jquery-ui.min.css";
|
import "../externals/jquery-ui.min.css";
|
||||||
import "../externals/jquery-ui.min.js";
|
import "../externals/jquery-ui.min.js";
|
||||||
import "../externals/jquery-ui.structure.min.css";
|
import "../externals/jquery-ui.structure.min.css";
|
||||||
|
@ -18,21 +18,19 @@ import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
import { Platform } from "ConfigContext";
|
import { Platform } from "ConfigContext";
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import "../images/favicon.ico";
|
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
|
import "../images/favicon.ico";
|
||||||
|
import "../less/TableStyles/CustomizeColumns.less";
|
||||||
|
import "../less/TableStyles/EntityEditor.less";
|
||||||
|
import "../less/TableStyles/fulldatatables.less";
|
||||||
|
import "../less/TableStyles/queryBuilder.less";
|
||||||
import "../less/documentDB.less";
|
import "../less/documentDB.less";
|
||||||
import "../less/forms.less";
|
import "../less/forms.less";
|
||||||
import "../less/infobox.less";
|
import "../less/infobox.less";
|
||||||
import "../less/menus.less";
|
import "../less/menus.less";
|
||||||
import "../less/messagebox.less";
|
import "../less/messagebox.less";
|
||||||
import "../less/resourceTree.less";
|
import "../less/resourceTree.less";
|
||||||
import "../less/TableStyles/CustomizeColumns.less";
|
|
||||||
import "../less/TableStyles/EntityEditor.less";
|
|
||||||
import "../less/TableStyles/fulldatatables.less";
|
|
||||||
import "../less/TableStyles/queryBuilder.less";
|
|
||||||
import "../less/tree.less";
|
import "../less/tree.less";
|
||||||
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
||||||
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
||||||
|
@ -55,11 +53,11 @@ import "./Explorer/Panes/PanelComponent.less";
|
||||||
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
||||||
import "./Explorer/SplashScreen/SplashScreen.less";
|
import "./Explorer/SplashScreen/SplashScreen.less";
|
||||||
import { Tabs } from "./Explorer/Tabs/Tabs";
|
import { Tabs } from "./Explorer/Tabs/Tabs";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
|
||||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
|
||||||
import "./Libs/jquery";
|
import "./Libs/jquery";
|
||||||
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
||||||
import "./Shared/appInsights";
|
import "./Shared/appInsights";
|
||||||
|
import { useConfig } from "./hooks/useConfig";
|
||||||
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
|
@ -67,7 +65,6 @@ const App: React.FunctionComponent = () => {
|
||||||
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
|
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
|
||||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||||
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
||||||
const shouldShowModal = useQueryCopilot((state) => state.showFeedbackModal);
|
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
if (config?.platform === Platform.Fabric) {
|
if (config?.platform === Platform.Fabric) {
|
||||||
|
@ -136,7 +133,6 @@ const App: React.FunctionComponent = () => {
|
||||||
{<SQLQuickstartTutorial />}
|
{<SQLQuickstartTutorial />}
|
||||||
{<MongoQuickstartTutorial />}
|
{<MongoQuickstartTutorial />}
|
||||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||||
{shouldShowModal && <QueryCopilotFeedbackModal explorer={explorer} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -112,7 +112,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||||
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
||||||
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
||||||
enableCopilot: "true" === get("enablecopilot", "true"),
|
enableCopilot: "true" === get("enablecopilot", "true"),
|
||||||
copilotVersion: get("copilotversion") ?? "v1.0",
|
copilotVersion: get("copilotversion") ?? "v2.0",
|
||||||
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
|
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
|
||||||
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
|
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
|
||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
|
|
|
@ -133,6 +133,10 @@ export enum Action {
|
||||||
CompleteUITour,
|
CompleteUITour,
|
||||||
OpenQueryCopilotFromSplashScreen,
|
OpenQueryCopilotFromSplashScreen,
|
||||||
OpenQueryCopilotFromNewQuery,
|
OpenQueryCopilotFromNewQuery,
|
||||||
|
ActivateQueryCopilot,
|
||||||
|
DeactivateQueryCopilot,
|
||||||
|
QueryGenerationFromCopilotPrompt,
|
||||||
|
QueryEdited,
|
||||||
ExecuteQueryGeneratedFromQueryCopilot,
|
ExecuteQueryGeneratedFromQueryCopilot,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
scheduleRefreshDatabaseResourceToken,
|
scheduleRefreshDatabaseResourceToken,
|
||||||
} from "Platform/Fabric/FabricUtil";
|
} from "Platform/Fabric/FabricUtil";
|
||||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
|
@ -79,7 +80,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||||
if (explorer) {
|
if (explorer) {
|
||||||
applyExplorerBindings(explorer);
|
applyExplorerBindings(explorer);
|
||||||
if (userContext.features.enableCopilot) {
|
if (userContext.features.enableCopilot) {
|
||||||
updateContextForSampleData(explorer);
|
updateContextForCopilot(explorer).then(() => updateContextForSampleData(explorer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [explorer]);
|
}, [explorer]);
|
||||||
|
@ -554,12 +555,23 @@ interface PortalMessage {
|
||||||
inputs?: DataExplorerInputsFrame;
|
inputs?: DataExplorerInputsFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateContextForCopilot(explorer: Explorer): Promise<void> {
|
||||||
|
await explorer.configureCopilot();
|
||||||
|
}
|
||||||
|
|
||||||
async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
||||||
if (!userContext.features.enableCopilot) {
|
const copilotEnabled =
|
||||||
|
userContext.apiType === "SQL" && userContext.features.enableCopilot && useQueryCopilot.getState().copilotEnabled;
|
||||||
|
|
||||||
|
if (!copilotEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = createUri(`${configContext.BACKEND_ENDPOINT}`, `/api/tokens/sampledataconnection`);
|
const sampleDatabaseEndpoint = useQueryCopilot.getState().copilotUserDBEnabled
|
||||||
|
? `/api/tokens/sampledataconnection/v2`
|
||||||
|
: `/api/tokens/sampledataconnection`;
|
||||||
|
|
||||||
|
const url = createUri(`${configContext.BACKEND_ENDPOINT}`, sampleDatabaseEndpoint);
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||||
import { QueryResults } from "Contracts/ViewModels";
|
import { QueryResults } from "Contracts/ViewModels";
|
||||||
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { CopilotMessage, CopilotSchemaAllocationInfo } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { guid } from "Explorer/Tables/Utilities";
|
import { guid } from "Explorer/Tables/Utilities";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
|
@ -8,6 +8,8 @@ import * as DataModels from "../Contracts/DataModels";
|
||||||
import { ContainerInfo } from "../Contracts/DataModels";
|
import { ContainerInfo } from "../Contracts/DataModels";
|
||||||
|
|
||||||
export interface QueryCopilotState {
|
export interface QueryCopilotState {
|
||||||
|
copilotEnabled: boolean;
|
||||||
|
copilotUserDBEnabled: boolean;
|
||||||
generatedQuery: string;
|
generatedQuery: string;
|
||||||
likeQuery: boolean;
|
likeQuery: boolean;
|
||||||
userPrompt: string;
|
userPrompt: string;
|
||||||
|
@ -40,8 +42,14 @@ export interface QueryCopilotState {
|
||||||
showExplanationBubble: boolean;
|
showExplanationBubble: boolean;
|
||||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||||
containerStatus: ContainerInfo;
|
containerStatus: ContainerInfo;
|
||||||
|
schemaAllocationInfo: CopilotSchemaAllocationInfo;
|
||||||
isAllocatingContainer: boolean;
|
isAllocatingContainer: boolean;
|
||||||
|
copilotEnabledforExecution: boolean;
|
||||||
|
|
||||||
|
getState?: () => QueryCopilotState;
|
||||||
|
|
||||||
|
setCopilotEnabled: (copilotEnabled: boolean) => void;
|
||||||
|
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => void;
|
||||||
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void;
|
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void;
|
||||||
closeFeedbackModal: () => void;
|
closeFeedbackModal: () => void;
|
||||||
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void;
|
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void;
|
||||||
|
@ -76,6 +84,8 @@ export interface QueryCopilotState {
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||||
setContainerStatus: (containerStatus: ContainerInfo) => void;
|
setContainerStatus: (containerStatus: ContainerInfo) => void;
|
||||||
setIsAllocatingContainer: (isAllocatingContainer: boolean) => void;
|
setIsAllocatingContainer: (isAllocatingContainer: boolean) => void;
|
||||||
|
setSchemaAllocationInfo: (schemaAllocationInfo: CopilotSchemaAllocationInfo) => void;
|
||||||
|
setCopilotEnabledforExecution: (copilotEnabledforExecution: boolean) => void;
|
||||||
|
|
||||||
resetContainerConnection: () => void;
|
resetContainerConnection: () => void;
|
||||||
resetQueryCopilotStates: () => void;
|
resetQueryCopilotStates: () => void;
|
||||||
|
@ -84,6 +94,8 @@ export interface QueryCopilotState {
|
||||||
type QueryCopilotStore = UseStore<QueryCopilotState>;
|
type QueryCopilotStore = UseStore<QueryCopilotState>;
|
||||||
|
|
||||||
export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
||||||
|
copilotEnabled: false,
|
||||||
|
copilotUserDBEnabled: false,
|
||||||
generatedQuery: "",
|
generatedQuery: "",
|
||||||
likeQuery: false,
|
likeQuery: false,
|
||||||
userPrompt: "",
|
userPrompt: "",
|
||||||
|
@ -124,8 +136,15 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
||||||
durationLeftInMinutes: undefined,
|
durationLeftInMinutes: undefined,
|
||||||
phoenixServerInfo: undefined,
|
phoenixServerInfo: undefined,
|
||||||
},
|
},
|
||||||
|
schemaAllocationInfo: {
|
||||||
|
databaseId: undefined,
|
||||||
|
containerId: undefined,
|
||||||
|
},
|
||||||
isAllocatingContainer: false,
|
isAllocatingContainer: false,
|
||||||
|
copilotEnabledforExecution: false,
|
||||||
|
|
||||||
|
setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }),
|
||||||
|
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }),
|
||||||
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
|
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
|
||||||
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
|
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
|
||||||
closeFeedbackModal: () => set({ showFeedbackModal: false }),
|
closeFeedbackModal: () => set({ showFeedbackModal: false }),
|
||||||
|
@ -163,6 +182,8 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
||||||
set({ notebookServerInfo }),
|
set({ notebookServerInfo }),
|
||||||
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
||||||
setIsAllocatingContainer: (isAllocatingContainer: boolean) => set({ isAllocatingContainer }),
|
setIsAllocatingContainer: (isAllocatingContainer: boolean) => set({ isAllocatingContainer }),
|
||||||
|
setSchemaAllocationInfo: (schemaAllocationInfo: CopilotSchemaAllocationInfo) => set({ schemaAllocationInfo }),
|
||||||
|
setCopilotEnabledforExecution: (copilotEnabledforExecution: boolean) => set({ copilotEnabledforExecution }),
|
||||||
|
|
||||||
resetContainerConnection: (): void => {
|
resetContainerConnection: (): void => {
|
||||||
useTabs.getState().closeAllNotebookTabs(true);
|
useTabs.getState().closeAllNotebookTabs(true);
|
||||||
|
@ -173,6 +194,10 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
||||||
durationLeftInMinutes: undefined,
|
durationLeftInMinutes: undefined,
|
||||||
phoenixServerInfo: undefined,
|
phoenixServerInfo: undefined,
|
||||||
});
|
});
|
||||||
|
useQueryCopilot.getState().setSchemaAllocationInfo({
|
||||||
|
databaseId: undefined,
|
||||||
|
containerId: undefined,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
resetQueryCopilotStates: () => {
|
resetQueryCopilotStates: () => {
|
||||||
|
@ -217,6 +242,10 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
||||||
durationLeftInMinutes: undefined,
|
durationLeftInMinutes: undefined,
|
||||||
phoenixServerInfo: undefined,
|
phoenixServerInfo: undefined,
|
||||||
},
|
},
|
||||||
|
schemaAllocationInfo: {
|
||||||
|
databaseId: undefined,
|
||||||
|
containerId: undefined,
|
||||||
|
},
|
||||||
isAllocatingContainer: false,
|
isAllocatingContainer: false,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@ import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
|
||||||
import TabsBase from "../Explorer/Tabs/TabsBase";
|
import TabsBase from "../Explorer/Tabs/TabsBase";
|
||||||
import { Platform, configContext } from "./../ConfigContext";
|
import { Platform, configContext } from "./../ConfigContext";
|
||||||
|
|
||||||
interface TabsState {
|
export interface TabsState {
|
||||||
openedTabs: TabsBase[];
|
openedTabs: TabsBase[];
|
||||||
openedReactTabs: ReactTabKind[];
|
openedReactTabs: ReactTabKind[];
|
||||||
activeTab: TabsBase | undefined;
|
activeTab: TabsBase | undefined;
|
||||||
|
|
Loading…
Reference in New Issue