mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-30 22:31:56 +00:00
Compare commits
31 Commits
upload_doc
...
users/artr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f7783f3f9 | ||
|
|
daa26d289b | ||
|
|
879cb08949 | ||
|
|
92f43c28a7 | ||
|
|
5f0c7bcea2 | ||
|
|
fa55d528ad | ||
|
|
c873fed7aa | ||
|
|
7ec5290293 | ||
|
|
4005128211 | ||
|
|
38d13cc74e | ||
|
|
4ab93a5a32 | ||
|
|
44e25c0769 | ||
|
|
8ea8f0230f | ||
|
|
655b998b84 | ||
|
|
9e2f6a9c89 | ||
|
|
42e11d5160 | ||
|
|
10037d844e | ||
|
|
13434715b3 | ||
|
|
676434cac5 | ||
|
|
3d5580865a | ||
|
|
7375cc717c | ||
|
|
de3e56bb99 | ||
|
|
53cd78452b | ||
|
|
fb6eb635c1 | ||
|
|
fb6c0caca6 | ||
|
|
e9f3c64239 | ||
|
|
bb6ee5deec | ||
|
|
5796b28045 | ||
|
|
9635d14599 | ||
|
|
c58d5765c2 | ||
|
|
708f4316b4 |
@@ -230,7 +230,7 @@ input::-webkit-inner-spin-button {
|
|||||||
.advanced-options-panel .advanced-options .select .select-options-link {
|
.advanced-options-panel .advanced-options .select .select-options-link {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-panel .row .column-headers .Field {
|
.query-panel .row .column-headers .Field {
|
||||||
|
|||||||
@@ -28,4 +28,4 @@
|
|||||||
.clickDisabled {
|
.clickDisabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -164,6 +164,7 @@
|
|||||||
"jest": "26.6.3",
|
"jest": "26.6.3",
|
||||||
"jest-canvas-mock": "2.3.1",
|
"jest-canvas-mock": "2.3.1",
|
||||||
"jest-playwright-preset": "1.5.1",
|
"jest-playwright-preset": "1.5.1",
|
||||||
|
"jest-react-hooks-shallow": "1.5.1",
|
||||||
"jest-trx-results-processor": "0.0.7",
|
"jest-trx-results-processor": "0.0.7",
|
||||||
"less": "3.8.1",
|
"less": "3.8.1",
|
||||||
"less-loader": "4.1.0",
|
"less-loader": "4.1.0",
|
||||||
@@ -19578,6 +19579,16 @@
|
|||||||
"node": ">=8.9.0"
|
"node": ">=8.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-react-hooks-shallow": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/jest-react-hooks-shallow/-/jest-react-hooks-shallow-1.5.1.tgz",
|
||||||
|
"integrity": "sha1-1SI4EGG7nf9WcfhLBT0LPxy+N/k=",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-regex-util": {
|
"node_modules/jest-regex-util": {
|
||||||
"version": "24.9.0",
|
"version": "24.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
|
||||||
@@ -48608,6 +48619,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jest-react-hooks-shallow": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/jest-react-hooks-shallow/-/jest-react-hooks-shallow-1.5.1.tgz",
|
||||||
|
"integrity": "sha1-1SI4EGG7nf9WcfhLBT0LPxy+N/k=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"react": "^16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"jest-regex-util": {
|
"jest-regex-util": {
|
||||||
"version": "24.9.0",
|
"version": "24.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
"jest": "26.6.3",
|
"jest": "26.6.3",
|
||||||
"jest-canvas-mock": "2.3.1",
|
"jest-canvas-mock": "2.3.1",
|
||||||
"jest-playwright-preset": "1.5.1",
|
"jest-playwright-preset": "1.5.1",
|
||||||
|
"jest-react-hooks-shallow": "1.5.1",
|
||||||
"jest-trx-results-processor": "0.0.7",
|
"jest-trx-results-processor": "0.0.7",
|
||||||
"less": "3.8.1",
|
"less": "3.8.1",
|
||||||
"less-loader": "4.1.0",
|
"less-loader": "4.1.0",
|
||||||
@@ -232,4 +233,4 @@
|
|||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,24 +435,89 @@ export const QueryCopilotSampleContainerId = "SampleContainer";
|
|||||||
export const QueryCopilotSampleContainerSchema = {
|
export const QueryCopilotSampleContainerSchema = {
|
||||||
product: {
|
product: {
|
||||||
sampleData: {
|
sampleData: {
|
||||||
id: "de6fadec-0384-43c8-93ea-16c0170b5460",
|
id: "c415e70f-9bf5-4cda-aebe-a290cb8b94c2",
|
||||||
name: "Premium Phone Mini (Red)",
|
name: "Amazing Phone 3000 (Black)",
|
||||||
price: 652.04,
|
price: 223.33,
|
||||||
category: "Electronics",
|
category: "Electronics",
|
||||||
description:
|
description:
|
||||||
"This Premium Phone Mini (Red) is designed by the company under agreement with the FCC, so we'd give it a pass in either direction, but no one should be using this handset without a compatible handset. All in all, this phone is one of the most affordable Android handsets out there at $100. Check them out.\n\n9. HTC One M9 4",
|
"This Amazing Phone 3000 (Black) is made of black metal! It has a very well made aluminum body and it feels very comfortable. We loved the sound that comes out of it! Also, the design of the phone was a little loose at first because I was using the camera and felt uncomfortable wearing it. The phone is actually made slightly smaller than these photos! This is due to the addition of a 3.3mm filter",
|
||||||
stock: 74,
|
stock: 84,
|
||||||
countryOfOrigin: "Mexico",
|
countryOfOrigin: "USA",
|
||||||
firstAvailable: "2018-07-07 17:42:28",
|
firstAvailable: "2018-09-07 19:41:44",
|
||||||
priceHistory: [592.81],
|
priceHistory: [238.68, 234.7, 221.49, 205.88, 220.15],
|
||||||
customerRatings: [
|
customerRatings: [
|
||||||
{ username: "dannyhowell", stars: 1, date: "2022-03-12 17:01:23", verifiedUser: true },
|
{
|
||||||
{ username: "lindsay26", stars: 1, date: "2022-12-29 07:18:20", verifiedUser: false },
|
username: "steven66",
|
||||||
{ username: "smithmiguel", stars: 3, date: "2022-09-08 11:43:27", verifiedUser: false },
|
firstName: "Carol",
|
||||||
{ username: "julie07", stars: 3, date: "2021-03-14 23:54:10", verifiedUser: true },
|
gender: "female",
|
||||||
{ username: "kelly93", stars: 3, date: "2022-11-05 12:45:43", verifiedUser: false },
|
lastName: "Shelton",
|
||||||
{ username: "katherinereynolds", stars: 2, date: "2022-09-14 11:49:36", verifiedUser: false },
|
age: "25-35",
|
||||||
{ username: "chandlerkenneth", stars: 1, date: "2022-01-14 12:14:43", verifiedUser: true },
|
area: "suburban",
|
||||||
|
address: "261 Collins Burgs Apt. 332\nNorth Taylor, NM 32268",
|
||||||
|
stars: 5,
|
||||||
|
date: "2021-04-22 13:42:14",
|
||||||
|
verifiedUser: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "khudson",
|
||||||
|
firstName: "Ronald",
|
||||||
|
gender: "male",
|
||||||
|
lastName: "Webb",
|
||||||
|
age: "18-24",
|
||||||
|
area: "suburban",
|
||||||
|
address: "9912 Parker Court Apt. 068\nNorth Austin, HI 76225",
|
||||||
|
stars: 5,
|
||||||
|
date: "2021-02-07 07:00:22",
|
||||||
|
verifiedUser: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "lfrancis",
|
||||||
|
firstName: "Brady",
|
||||||
|
gender: "male",
|
||||||
|
lastName: "Wright",
|
||||||
|
age: "35-45",
|
||||||
|
area: "urban",
|
||||||
|
address: "PSC 5437, Box 3159\nAPO AA 26385",
|
||||||
|
stars: 2,
|
||||||
|
date: "2022-02-23 21:40:10",
|
||||||
|
verifiedUser: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "nicolemartinez",
|
||||||
|
firstName: "Megan",
|
||||||
|
gender: "female",
|
||||||
|
lastName: "Tran",
|
||||||
|
age: "18-24",
|
||||||
|
area: "rural",
|
||||||
|
address: "7445 Salazar Brooks\nNew Sarah, PW 18097",
|
||||||
|
stars: 4,
|
||||||
|
date: "2021-09-01 22:21:40",
|
||||||
|
verifiedUser: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "uguzman",
|
||||||
|
firstName: "Deanna",
|
||||||
|
gender: "female",
|
||||||
|
lastName: "Campbell",
|
||||||
|
age: "18-24",
|
||||||
|
area: "urban",
|
||||||
|
address: "41104 Moreno Fort Suite 872\nPort Michaelbury, AK 48712",
|
||||||
|
stars: 1,
|
||||||
|
date: "2022-03-07 02:23:14",
|
||||||
|
verifiedUser: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "rebeccahunt",
|
||||||
|
firstName: "Jared",
|
||||||
|
gender: "male",
|
||||||
|
lastName: "Lopez",
|
||||||
|
age: "18-24",
|
||||||
|
area: "rural",
|
||||||
|
address: "392 Morgan Village Apt. 785\nGreenshire, CT 05921",
|
||||||
|
stars: 5,
|
||||||
|
date: "2021-04-17 04:17:49",
|
||||||
|
verifiedUser: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
rareProperty: true,
|
rareProperty: true,
|
||||||
},
|
},
|
||||||
@@ -494,6 +559,24 @@ export const QueryCopilotSampleContainerSchema = {
|
|||||||
username: {
|
username: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
firstName: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
gender: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
stars: {
|
stars: {
|
||||||
type: "number",
|
type: "number",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
/>
|
/>
|
||||||
{!isEntityValueDisable && (
|
{!isEntityValueDisable && (
|
||||||
<TooltipHost content="Edit property" id="editTooltip">
|
<TooltipHost content="Edit property" id="editTooltip">
|
||||||
<div tabIndex={0}>
|
<div>
|
||||||
<Image
|
<Image
|
||||||
{...imageProps}
|
{...imageProps}
|
||||||
src={EditIcon}
|
src={EditIcon}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedArmEndpoints,
|
|
||||||
allowedBackendEndpoints,
|
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMongoBackendEndpoints,
|
allowedMongoBackendEndpoints,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
|
defaultAllowedArmEndpoints,
|
||||||
|
defaultAllowedBackendEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointValidation";
|
} from "Utils/EndpointValidation";
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ export enum Platform {
|
|||||||
|
|
||||||
export interface ConfigContext {
|
export interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
|
allowedArmEndpoints: ReadonlyArray<string>;
|
||||||
|
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
proxyPath?: string;
|
proxyPath?: string;
|
||||||
@@ -49,6 +51,8 @@ export interface ConfigContext {
|
|||||||
// Default configuration
|
// Default configuration
|
||||||
let configContext: Readonly<ConfigContext> = {
|
let configContext: Readonly<ConfigContext> = {
|
||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
|
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||||
|
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||||
allowedParentFrameOrigins: [
|
allowedParentFrameOrigins: [
|
||||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||||
@@ -77,7 +81,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
|
|
||||||
export function resetConfigContext(): void {
|
export function resetConfigContext(): void {
|
||||||
if (process.env.NODE_ENV !== "test") {
|
if (process.env.NODE_ENV !== "test") {
|
||||||
throw new Error("resetConfigContext can only becalled in a test environment");
|
throw new Error("resetConfigContext can only be called in a test environment");
|
||||||
}
|
}
|
||||||
configContext = {} as ConfigContext;
|
configContext = {} as ConfigContext;
|
||||||
}
|
}
|
||||||
@@ -87,7 +91,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) {
|
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
||||||
delete newContext.ARM_ENDPOINT;
|
delete newContext.ARM_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +111,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.ARCADIA_ENDPOINT;
|
delete newContext.ARCADIA_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
|
if (
|
||||||
|
!validateEndpoint(
|
||||||
|
newContext.BACKEND_ENDPOINT,
|
||||||
|
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints
|
||||||
|
)
|
||||||
|
) {
|
||||||
delete newContext.BACKEND_ENDPOINT;
|
delete newContext.BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +139,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
Object.assign(configContext, newContext);
|
Object.assign(configContext, newContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Injected for local develpment. These will be removed in the production bundle by webpack
|
// Injected for local development. These will be removed in the production bundle by webpack
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
const port: string = process.env.PORT || "1234";
|
const port: string = process.env.PORT || "1234";
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import AddCollectionIcon from "../../images/AddCollection.svg";
|
import AddCollectionIcon from "../../images/AddCollection.svg";
|
||||||
@@ -146,7 +148,10 @@ export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] =>
|
|||||||
if (userContext.apiType === "SQL") {
|
if (userContext.apiType === "SQL") {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: AddSqlQueryIcon,
|
iconSrc: AddSqlQueryIcon,
|
||||||
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot),
|
onClick: () => {
|
||||||
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||||
|
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
|
||||||
|
},
|
||||||
label: "New SQL Query",
|
label: "New SQL Query",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export class AccordionComponent extends React.Component<AccordionComponentProps>
|
|||||||
export interface AccordionItemComponentProps {
|
export interface AccordionItemComponentProps {
|
||||||
title: string;
|
title: string;
|
||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
|
containerStyles?: React.CSSProperties;
|
||||||
styles?: React.CSSProperties;
|
styles?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +55,9 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const { styles } = this.props;
|
const { containerStyles, styles } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="accordionItemContainer">
|
<div className="accordionItemContainer" style={{ ...containerStyles }}>
|
||||||
<div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}>
|
<div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}>
|
||||||
{this.renderCollapseExpandIcon()}
|
{this.renderCollapseExpandIcon()}
|
||||||
{this.props.title}
|
{this.props.title}
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
|||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
this.createEditor(this.configureEditor.bind(this));
|
this.createEditor(this.configureEditor.bind(this));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const suggestionWidget = this.editor?.getDomNode()?.querySelector(".suggest-widget") as HTMLElement;
|
||||||
|
if (suggestionWidget) {
|
||||||
|
suggestionWidget.style.display = "none";
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(previous: EditorReactProps) {
|
public componentDidUpdate(previous: EditorReactProps) {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Link } from "@fluentui/react/lib/Link";
|
import { Link } from "@fluentui/react/lib/Link";
|
||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
|
import { Platform } from "ConfigContext";
|
||||||
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
@@ -23,7 +26,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
|||||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||||
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 { userContext } from "../UserContext";
|
import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext";
|
||||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||||
@@ -258,6 +261,45 @@ export default class Explorer {
|
|||||||
// TODO: return result
|
// TODO: return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getRandomInt(max: number) {
|
||||||
|
return Math.floor(Math.random() * max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public openNPSSurveyDialog(): void {
|
||||||
|
if (!Platform.Portal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NINETY_DAYS_IN_MS = 7776000000;
|
||||||
|
const ONE_DAY_IN_MS = 86400000;
|
||||||
|
const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs(
|
||||||
|
userContext.databaseAccount?.systemData?.createdAt || "",
|
||||||
|
NINETY_DAYS_IN_MS
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try Cosmos DB subscription - survey shown to random 25% of users at day 1 in Data Explorer.
|
||||||
|
if (userContext.isTryCosmosDBSubscription) {
|
||||||
|
if (
|
||||||
|
isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS) &&
|
||||||
|
this.getRandomInt(100) < 25
|
||||||
|
) {
|
||||||
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// An existing account is lesser than 90 days old. For existing account show to random 10 % of users in Data Explorer.
|
||||||
|
if (isAccountNewerThanNinetyDays) {
|
||||||
|
if (this.getRandomInt(100) < 10) {
|
||||||
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// An existing account is greater than 90 days. For existing account show to random 25 % of users in Data Explorer.
|
||||||
|
if (this.getRandomInt(100) < 25) {
|
||||||
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async refreshDatabaseForResourceToken(): Promise<void> {
|
public async refreshDatabaseForResourceToken(): Promise<void> {
|
||||||
const databaseId = userContext.parsedResourceToken?.databaseId;
|
const databaseId = userContext.parsedResourceToken?.databaseId;
|
||||||
const collectionId = userContext.parsedResourceToken?.collectionId;
|
const collectionId = userContext.parsedResourceToken?.collectionId;
|
||||||
@@ -1291,7 +1333,7 @@ export default class Explorer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection);
|
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
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";
|
||||||
@@ -268,7 +270,6 @@ function createNewCollectionGroup(container: Explorer): CommandButtonComponentPr
|
|||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
id: "createNewContainerCommandButton",
|
id: "createNewContainerCommandButton",
|
||||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +315,6 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +328,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
|||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
|
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
|
||||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||||
|
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
|
||||||
} else {
|
} else {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||||
@@ -493,7 +494,7 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
|
|||||||
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
|
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: false,
|
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1425,6 +1425,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
this.setState({ isExecuting: false });
|
this.setState({ isExecuting: false });
|
||||||
TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey);
|
TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey);
|
||||||
useSidePanel.getState().closeSidePanel();
|
useSidePanel.getState().closeSidePanel();
|
||||||
|
// open NPS Survey Dialog once the collection is created
|
||||||
|
if (userContext.features.enableNPSSurvey) {
|
||||||
|
this.props.explorer.openNPSSurveyDialog();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage: string = getErrorMessage(error);
|
const errorMessage: string = getErrorMessage(error);
|
||||||
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });
|
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } fro
|
|||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import React, { FunctionComponent, useState } from "react";
|
|
||||||
import * as SharedConstants from "Shared/Constants";
|
import * as SharedConstants from "Shared/Constants";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
||||||
@@ -291,7 +291,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
required={true}
|
required={true}
|
||||||
ariaLabel="addCollection-tableId"
|
ariaLabel="addCollection-table Id Create table"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Text, TextField } from "@fluentui/react";
|
import { Text, TextField } from "@fluentui/react";
|
||||||
import { Areas } from "Common/Constants";
|
import { Areas } from "Common/Constants";
|
||||||
import { deleteCollection } from "Common/dataAccess/deleteCollection";
|
|
||||||
import DeleteFeedback from "Common/DeleteFeedback";
|
import DeleteFeedback from "Common/DeleteFeedback";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
|
import { deleteCollection } from "Common/dataAccess/deleteCollection";
|
||||||
import { Collection } from "Contracts/ViewModels";
|
import { Collection } from "Contracts/ViewModels";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import { useTabs } from "hooks/useTabs";
|
|
||||||
import React, { FunctionComponent, useState } from "react";
|
|
||||||
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
||||||
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 { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
@@ -126,6 +126,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
setInputCollectionName(newInput);
|
setInputCollectionName(newInput);
|
||||||
}}
|
}}
|
||||||
ariaLabel={confirmContainer}
|
ariaLabel={confirmContainer}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{shouldRecordFeedback() && (
|
{shouldRecordFeedback() && (
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"fieldGroup": Object {
|
"fieldGroup": Object {
|
||||||
@@ -59,6 +60,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
resizable={true}
|
resizable={true}
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
theme={
|
theme={
|
||||||
@@ -338,7 +340,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
value=""
|
value=""
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField root-55"
|
className="ms-TextField is-required root-55"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField-wrapper"
|
className="ms-TextField-wrapper"
|
||||||
@@ -356,6 +358,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onInput={[Function]}
|
onInput={[Function]}
|
||||||
|
required={true}
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
setDatabaseInput(newInput);
|
setDatabaseInput(newInput);
|
||||||
}}
|
}}
|
||||||
ariaLabel={confirmDatabase}
|
ariaLabel={confirmDatabase}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isLastNonEmptyDatabase() && (
|
{isLastNonEmptyDatabase() && (
|
||||||
|
|||||||
@@ -112,6 +112,9 @@
|
|||||||
margin-top: 28px;
|
margin-top: 28px;
|
||||||
margin-left: 4px !important;
|
margin-left: 4px !important;
|
||||||
}
|
}
|
||||||
|
.addRemoveIcon [alt="editEntity"]:focus,.addRemoveIconLabel [alt="editEntity"]:focus{
|
||||||
|
border:1px dashed #605E5C
|
||||||
|
}
|
||||||
.addNewParamStyle {
|
.addNewParamStyle {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
margin-left: 5px !important;
|
margin-left: 5px !important;
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"fieldGroup": Object {
|
"fieldGroup": Object {
|
||||||
@@ -385,6 +386,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
resizable={true}
|
resizable={true}
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
theme={
|
theme={
|
||||||
@@ -663,7 +665,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
validateOnLoad={true}
|
validateOnLoad={true}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField root-58"
|
className="ms-TextField is-required root-58"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField-wrapper"
|
className="ms-TextField-wrapper"
|
||||||
@@ -681,6 +683,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onInput={[Function]}
|
onInput={[Function]}
|
||||||
|
required={true}
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { Checkbox, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react";
|
||||||
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
|
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
|
import { getUserEmail } from "Utils/UserUtils";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
jest.mock("Utils/UserUtils");
|
||||||
|
(getUserEmail as jest.Mock).mockResolvedValue("test@email.com");
|
||||||
|
|
||||||
|
jest.mock("Explorer/QueryCopilot/QueryCopilotUtilities");
|
||||||
|
submitFeedback as jest.Mock;
|
||||||
|
|
||||||
|
describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
it("shoud render and match snapshot", () => {
|
||||||
|
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
|
||||||
|
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||||
|
|
||||||
|
expect(wrapper.props().isOpen).toBeTruthy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close on cancel click", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||||
|
|
||||||
|
const cancelButton = wrapper.find(IconButton);
|
||||||
|
cancelButton.simulate("click");
|
||||||
|
wrapper.setProps({});
|
||||||
|
|
||||||
|
expect(wrapper.props().isOpen).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get user unput", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||||
|
const testUserInput = "test user input";
|
||||||
|
|
||||||
|
const userInput = wrapper.find(TextField).first();
|
||||||
|
userInput.simulate("change", {}, testUserInput);
|
||||||
|
|
||||||
|
expect(wrapper.find(TextField).first().props().value).toEqual(testUserInput);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should record user contact choice no", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||||
|
const contactAllowed = wrapper.find(ChoiceGroup);
|
||||||
|
|
||||||
|
contactAllowed.simulate("change", {}, { key: "no" });
|
||||||
|
|
||||||
|
expect(getUserEmail).toHaveBeenCalledTimes(3);
|
||||||
|
expect(wrapper.find(ChoiceGroup).props().selectedKey).toEqual("no");
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should record user contact choice yes", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||||
|
const contactAllowed = wrapper.find(ChoiceGroup);
|
||||||
|
|
||||||
|
contactAllowed.simulate("change", {}, { key: "yes" });
|
||||||
|
|
||||||
|
expect(getUserEmail).toHaveBeenCalledTimes(4);
|
||||||
|
expect(wrapper.find(ChoiceGroup).props().selectedKey).toEqual("yes");
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render dont show again button", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||||
|
|
||||||
|
const dontShowAgain = wrapper.find(Checkbox);
|
||||||
|
|
||||||
|
expect(dontShowAgain).toHaveLength(0);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render dont show again button and check it ", () => {
|
||||||
|
useQueryCopilot.getState().openFeedbackModal("test query", true, "test prompt");
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||||
|
|
||||||
|
const dontShowAgain = wrapper.find(Checkbox);
|
||||||
|
dontShowAgain.simulate("change", {}, true);
|
||||||
|
|
||||||
|
expect(wrapper.find(Checkbox)).toHaveLength(1);
|
||||||
|
expect(wrapper.find(Checkbox).first().props().checked).toBeTruthy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cancel submission", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||||
|
|
||||||
|
const cancelButton = wrapper.find(DefaultButton);
|
||||||
|
cancelButton.simulate("click");
|
||||||
|
wrapper.setProps({});
|
||||||
|
|
||||||
|
expect(wrapper.props().isOpen).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should submit submission", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||||
|
|
||||||
|
const submitButton = wrapper.find(PrimaryButton);
|
||||||
|
submitButton.simulate("click");
|
||||||
|
wrapper.setProps({});
|
||||||
|
|
||||||
|
expect(submitFeedback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(submitFeedback).toHaveBeenCalledWith({
|
||||||
|
likeQuery: false,
|
||||||
|
generatedQuery: "",
|
||||||
|
userPrompt: "",
|
||||||
|
description: "",
|
||||||
|
contact: getUserEmail(),
|
||||||
|
});
|
||||||
|
expect(wrapper.props().isOpen).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
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: React.FC = (): JSX.Element => {
|
export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
@@ -28,6 +28,7 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
|||||||
const [description, setDescription] = React.useState<string>("");
|
const [description, setDescription] = React.useState<string>("");
|
||||||
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
||||||
const [contact, setContact] = React.useState<string>(getUserEmail());
|
const [contact, setContact] = React.useState<string>(getUserEmail());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={showFeedbackModal}>
|
<Modal isOpen={showFeedbackModal}>
|
||||||
<Stack style={{ padding: 24 }}>
|
<Stack style={{ padding: 24 }}>
|
||||||
@@ -77,11 +78,13 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
></ChoiceGroup>
|
></ChoiceGroup>
|
||||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. IT admins for your
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
|
||||||
organization will be able to view and manage your feedback data.{" "}
|
{
|
||||||
<Link href="" target="_blank">
|
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
|
||||||
Privacy statement
|
Privacy statement
|
||||||
</Link>
|
</Link>
|
||||||
|
}{" "}
|
||||||
|
for more information.
|
||||||
</Text>
|
</Text>
|
||||||
{likeQuery && (
|
{likeQuery && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
32
src/Explorer/QueryCopilot/Modal/WelcomeModal.test.tsx
Normal file
32
src/Explorer/QueryCopilot/Modal/WelcomeModal.test.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { withHooks } from "jest-react-hooks-shallow";
|
||||||
|
import React from "react";
|
||||||
|
import { WelcomeModal } from "./WelcomeModal";
|
||||||
|
|
||||||
|
describe("Query Copilot Welcome Modal snapshot test", () => {
|
||||||
|
it("should render when isOpen is true", () => {
|
||||||
|
withHooks(() => {
|
||||||
|
const spy = jest.spyOn(localStorage, "setItem");
|
||||||
|
spy.mockClear();
|
||||||
|
const wrapper = shallow(<WelcomeModal visible={true} />);
|
||||||
|
|
||||||
|
expect(wrapper.props().children.props.isOpen).toBeTruthy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spy).toHaveBeenLastCalledWith("hideWelcomeModal", "true");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should not render when isOpen is false", () => {
|
||||||
|
withHooks(() => {
|
||||||
|
const spy = jest.spyOn(localStorage, "setItem");
|
||||||
|
spy.mockClear();
|
||||||
|
const wrapper = shallow(<WelcomeModal visible={false} />);
|
||||||
|
|
||||||
|
expect(wrapper.props().children.props.isOpen).toBeFalsy();
|
||||||
|
|
||||||
|
expect(spy).not.toHaveBeenCalled();
|
||||||
|
expect(spy.mock.instances.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -42,8 +42,8 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontalAlign="center">
|
<Stack horizontalAlign="center">
|
||||||
<Stack.Item align="center">
|
<Stack.Item align="center" style={{ textAlign: "center" }}>
|
||||||
<Text className="title bold">Welcome to Copilot in CosmosDB</Text>
|
<Text className="title bold">Welcome to Query Copilot for Azure Cosmos DB (Private Preview)</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item align="center" className="text">
|
<Stack.Item align="center" className="text">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
@@ -52,7 +52,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
|||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem align="start">
|
<StackItem align="start">
|
||||||
<Text className="bold">
|
<Text className="bold">
|
||||||
Let copilot do the work for you
|
Let Query Copilot do the work for you
|
||||||
<br />
|
<br />
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
@@ -60,7 +60,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 href="">Learn more</Link>
|
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item align="center" className="text">
|
<Stack.Item align="center" className="text">
|
||||||
@@ -78,7 +78,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
|||||||
<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’s accurate and appropriate before using it.
|
||||||
<br />
|
<br />
|
||||||
<Link href="">Read preview terms</Link>
|
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item align="center" className="text">
|
<Stack.Item align="center" className="text">
|
||||||
@@ -88,15 +88,16 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
|||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem align="start">
|
<StackItem align="start">
|
||||||
<Text className="bold">
|
<Text className="bold">
|
||||||
Copilot currently works only a sample database
|
Query Copilot works on a sample database.
|
||||||
<br />
|
<br />
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text>
|
<Text>
|
||||||
Copilot is setup on a sample database we have configured for you at no cost
|
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you
|
||||||
|
at no cost.
|
||||||
<br />
|
<br />
|
||||||
<Link href="">Learn more</Link>
|
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,201 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is true 1`] = `
|
||||||
|
<Fragment>
|
||||||
|
<Modal
|
||||||
|
isBlocking={false}
|
||||||
|
isOpen={true}
|
||||||
|
onDismiss={[Function]}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="modalContentPadding"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
grow={4}
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="end"
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
className="exitPadding"
|
||||||
|
grow={1}
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="end"
|
||||||
|
verticalAlign="start"
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
className="previewMargin"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="preview"
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<CustomizedIconButton
|
||||||
|
ariaLabel="Exit"
|
||||||
|
className="exitIcon"
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Cancel",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
title="Exit"
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="center"
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
align="center"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"textAlign": "center",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="title bold"
|
||||||
|
>
|
||||||
|
Welcome to Query Copilot for Azure Cosmos DB (Private Preview)
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="center"
|
||||||
|
className="text"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
className="imageTextPadding"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="bold"
|
||||||
|
>
|
||||||
|
Let Query Copilot do the work for you
|
||||||
|
<br />
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
<Text>
|
||||||
|
Ask Copilot to generate a query by describing the query in your words.
|
||||||
|
<br />
|
||||||
|
<StyledLinkBase
|
||||||
|
href="http://aka.ms/cdb-copilot-learn-more"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="center"
|
||||||
|
className="text"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
className="imageTextPadding"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="bold"
|
||||||
|
>
|
||||||
|
Use your judgement
|
||||||
|
<br />
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
<Text>
|
||||||
|
AI-generated content can have mistakes. Make sure it’s accurate and appropriate before using it.
|
||||||
|
<br />
|
||||||
|
<StyledLinkBase
|
||||||
|
href="http://aka.ms/cdb-copilot-preview-terms"
|
||||||
|
>
|
||||||
|
Read preview terms
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="center"
|
||||||
|
className="text"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
className="imageTextPadding"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="bold"
|
||||||
|
>
|
||||||
|
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="http://aka.ms/cdb-copilot-learn-more"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
className="buttonPadding"
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<CustomizedPrimaryButton
|
||||||
|
className="tryButton"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Try Copilot
|
||||||
|
</CustomizedPrimaryButton>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
</Fragment>
|
||||||
|
`;
|
||||||
@@ -1,11 +1,48 @@
|
|||||||
|
import { IconButton } from "@fluentui/react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { any } from "underscore";
|
|
||||||
import { CopyPopup } from "./CopyPopup";
|
import { CopyPopup } from "./CopyPopup";
|
||||||
|
|
||||||
describe("Copy Popup snapshot test", () => {
|
describe("Copy Popup snapshot test", () => {
|
||||||
|
const setShowCopyPopupMock = jest.fn();
|
||||||
it("should render when showCopyPopup is true", () => {
|
it("should render when showCopyPopup is true", () => {
|
||||||
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={() => any} />);
|
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
expect(wrapper.prop("setShowCopyPopup")).toBeUndefined();
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should render when showCopyPopup is false", () => {
|
||||||
|
const wrapper = shallow(<CopyPopup showCopyPopup={false} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||||
|
expect(wrapper.prop("showCopyPopup")).toBeFalsy();
|
||||||
|
expect(wrapper.prop("setShowCopyPopup")).toBeUndefined();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setShowCopyPopup(false) when close button is clicked", () => {
|
||||||
|
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||||
|
|
||||||
|
const closeButton = wrapper.find(IconButton);
|
||||||
|
closeButton.props().onClick?.({} as React.MouseEvent<HTMLButtonElement, MouseEvent>);
|
||||||
|
|
||||||
|
expect(setShowCopyPopupMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have the correct inline styles", () => {
|
||||||
|
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||||
|
|
||||||
|
const stackStyle = wrapper.find("Stack").first().props().style;
|
||||||
|
|
||||||
|
expect(stackStyle).toEqual({
|
||||||
|
position: "fixed",
|
||||||
|
width: 345,
|
||||||
|
height: 66,
|
||||||
|
padding: 10,
|
||||||
|
gap: 5,
|
||||||
|
top: 75,
|
||||||
|
right: 20,
|
||||||
|
background: "#FFFFFF",
|
||||||
|
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.16)",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,113 @@
|
|||||||
import { shallow } from "enzyme";
|
import { mount, shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { any } from "underscore";
|
|
||||||
import { DeletePopup } from "./DeletePopup";
|
import { DeletePopup } from "./DeletePopup";
|
||||||
|
|
||||||
describe("Delete Popup snapshot test", () => {
|
describe("Delete Popup snapshot test", () => {
|
||||||
|
const setShowDeletePopupMock = jest.fn();
|
||||||
|
const setQueryMock = jest.fn();
|
||||||
|
const clearFeedbackMock = jest.fn();
|
||||||
|
const showFeedbackBarMock = jest.fn();
|
||||||
|
|
||||||
it("should render when showDeletePopup is true", () => {
|
it("should render when showDeletePopup is true", () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<DeletePopup
|
<DeletePopup
|
||||||
showDeletePopup={true}
|
showDeletePopup={true}
|
||||||
setShowDeletePopup={() => any}
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
setQuery={() => any}
|
setQuery={setQueryMock}
|
||||||
clearFeedback={() => any}
|
clearFeedback={clearFeedbackMock}
|
||||||
showFeedbackBar={() => any}
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
expect(wrapper.find("Modal").prop("isOpen")).toBeTruthy();
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not render when showDeletePopup is false", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={false}
|
||||||
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
|
setQuery={setQueryMock}
|
||||||
|
clearFeedback={clearFeedbackMock}
|
||||||
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.props().children.props.showDeletePopup).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setQuery with an empty string and setShowDeletePopup(false) when delete button is clicked", () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={true}
|
||||||
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
|
setQuery={setQueryMock}
|
||||||
|
clearFeedback={clearFeedbackMock}
|
||||||
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find("PrimaryButton").simulate("click");
|
||||||
|
|
||||||
|
expect(setQueryMock).toHaveBeenCalledWith("");
|
||||||
|
expect(setShowDeletePopupMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setShowDeletePopup(false) when close button is clicked", () => {
|
||||||
|
const setShowDeletePopupMock = jest.fn();
|
||||||
|
const wrapper = mount(
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={true}
|
||||||
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
|
setQuery={setQueryMock}
|
||||||
|
clearFeedback={clearFeedbackMock}
|
||||||
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find("DefaultButton").at(1).simulate("click");
|
||||||
|
|
||||||
|
expect(setShowDeletePopupMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the appropriate text content", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={true}
|
||||||
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
|
setQuery={setQueryMock}
|
||||||
|
clearFeedback={clearFeedbackMock}
|
||||||
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const textContent = wrapper
|
||||||
|
.find("Text")
|
||||||
|
.map((text, index) => <React.Fragment key={index}>{text.props().children}</React.Fragment>);
|
||||||
|
|
||||||
|
expect(textContent).toEqual([
|
||||||
|
<React.Fragment key={0}>
|
||||||
|
<b>Delete code?</b>
|
||||||
|
</React.Fragment>,
|
||||||
|
<React.Fragment key={1}>
|
||||||
|
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
|
||||||
|
</React.Fragment>,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have the correct inline style", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={true}
|
||||||
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
|
setQuery={setQueryMock}
|
||||||
|
clearFeedback={clearFeedbackMock}
|
||||||
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const stackStyle = wrapper.find("Stack[style]").props().style;
|
||||||
|
|
||||||
|
expect(stackStyle).toEqual({ padding: "16px 24px", height: "auto" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Copy Popup snapshot test should render when showCopyPopup is false 1`] = `<Fragment />`;
|
||||||
|
|
||||||
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
|
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
|
||||||
<Stack
|
<Stack
|
||||||
style={
|
style={
|
||||||
|
|||||||
@@ -1,5 +1,83 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Delete Popup snapshot test should not render when showDeletePopup is false 1`] = `
|
||||||
|
<Modal
|
||||||
|
isOpen={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"minHeight": "122px",
|
||||||
|
"minWidth": "880px",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": "auto",
|
||||||
|
"padding": "16px 24px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": "18px",
|
||||||
|
"height": 24,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<b>
|
||||||
|
Delete code?
|
||||||
|
</b>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginBottom": 20,
|
||||||
|
"marginTop": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
|
||||||
|
</Text>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="start"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CustomizedPrimaryButton
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 24,
|
||||||
|
"padding": "0px 20px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</CustomizedPrimaryButton>
|
||||||
|
<CustomizedDefaultButton
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 24,
|
||||||
|
"padding": "0px 20px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</CustomizedDefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Delete Popup snapshot test should render when showDeletePopup is true 1`] = `
|
exports[`Delete Popup snapshot test should render when showDeletePopup is true 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import { QueryCopilotTab } from "./QueryCopilotTab";
|
|||||||
|
|
||||||
describe("Query copilot tab snapshot test", () => {
|
describe("Query copilot tab snapshot test", () => {
|
||||||
it("should render with initial input", () => {
|
it("should render with initial input", () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
||||||
<QueryCopilotTab initialInput="Write a query to return all records in this table" explorer={new Explorer()} />
|
|
||||||
);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
Callout,
|
Callout,
|
||||||
CommandBarButton,
|
CommandBarButton,
|
||||||
@@ -17,16 +17,10 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import {
|
import { QueryCopilotSampleContainerId, QueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||||
QueryCopilotSampleContainerId,
|
|
||||||
QueryCopilotSampleContainerSchema,
|
|
||||||
QueryCopilotSampleDatabaseId,
|
|
||||||
} from "Common/Constants";
|
|
||||||
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, 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 { sampleDataClient } from "Common/SampleDataClient";
|
|
||||||
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
|
||||||
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
||||||
import { QueryResults } from "Contracts/ViewModels";
|
import { QueryResults } from "Contracts/ViewModels";
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
@@ -37,9 +31,11 @@ import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
|||||||
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/QueryCopilotUtilities";
|
import { querySampleDocuments, submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/SamplePrompts/SamplePrompts";
|
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/SamplePrompts/SamplePrompts";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -50,7 +46,6 @@ import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
|||||||
import HintIcon from "../../../images/Hint.svg";
|
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 SamplePromptsIcon from "../../../images/SamplePromptsIcon.svg";
|
|
||||||
import XErrorMessage from "../../../images/X-errorMessage.svg";
|
import XErrorMessage from "../../../images/X-errorMessage.svg";
|
||||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
@@ -61,7 +56,6 @@ interface SuggestedPrompt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface QueryCopilotTabProps {
|
interface QueryCopilotTabProps {
|
||||||
initialInput: string;
|
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,31 +72,50 @@ const promptStyles: IButtonStyles = {
|
|||||||
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({ explorer }: QueryCopilotTabProps): JSX.Element => {
|
||||||
initialInput,
|
|
||||||
explorer,
|
|
||||||
}: QueryCopilotTabProps): JSX.Element => {
|
|
||||||
const hideFeedbackModalForLikedQueries = useQueryCopilot((state) => state.hideFeedbackModalForLikedQueries);
|
|
||||||
const [userPrompt, setUserPrompt] = useState<string>(initialInput || "");
|
|
||||||
const [generatedQuery, setGeneratedQuery] = useState<string>("");
|
|
||||||
const [query, setQuery] = useState<string>("");
|
|
||||||
const [selectedQuery, setSelectedQuery] = useState<string>("");
|
|
||||||
const [isGeneratingQuery, setIsGeneratingQuery] = useState<boolean>(false);
|
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
|
||||||
const [likeQuery, setLikeQuery] = useState<boolean>();
|
|
||||||
const [dislikeQuery, setDislikeQuery] = useState<boolean>();
|
|
||||||
const [showCallout, setShowCallout] = useState<boolean>(false);
|
|
||||||
const [showSamplePrompts, setShowSamplePrompts] = useState<boolean>(false);
|
|
||||||
const [queryIterator, setQueryIterator] = useState<MinimalQueryIterator>();
|
|
||||||
const [queryResults, setQueryResults] = useState<QueryResults>();
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
||||||
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
||||||
const inputEdited = useRef(false);
|
const inputEdited = useRef(false);
|
||||||
const [isSamplePromptsOpen, setIsSamplePromptsOpen] = useState<boolean>(false);
|
const {
|
||||||
const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false);
|
hideFeedbackModalForLikedQueries,
|
||||||
const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false);
|
userPrompt,
|
||||||
const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false);
|
setUserPrompt,
|
||||||
const [showErrorMessageBar, setShowErrorMessageBar] = useState<boolean>(false);
|
generatedQuery,
|
||||||
|
setGeneratedQuery,
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
|
selectedQuery,
|
||||||
|
setSelectedQuery,
|
||||||
|
isGeneratingQuery,
|
||||||
|
setIsGeneratingQuery,
|
||||||
|
isExecuting,
|
||||||
|
setIsExecuting,
|
||||||
|
likeQuery,
|
||||||
|
setLikeQuery,
|
||||||
|
dislikeQuery,
|
||||||
|
setDislikeQuery,
|
||||||
|
showCallout,
|
||||||
|
setShowCallout,
|
||||||
|
showSamplePrompts,
|
||||||
|
setShowSamplePrompts,
|
||||||
|
queryIterator,
|
||||||
|
setQueryIterator,
|
||||||
|
queryResults,
|
||||||
|
setQueryResults,
|
||||||
|
errorMessage,
|
||||||
|
setErrorMessage,
|
||||||
|
isSamplePromptsOpen,
|
||||||
|
setIsSamplePromptsOpen,
|
||||||
|
showDeletePopup,
|
||||||
|
setShowDeletePopup,
|
||||||
|
showFeedbackBar,
|
||||||
|
setShowFeedbackBar,
|
||||||
|
showCopyPopup,
|
||||||
|
setshowCopyPopup,
|
||||||
|
showErrorMessageBar,
|
||||||
|
setShowErrorMessageBar,
|
||||||
|
generatedQueryComments,
|
||||||
|
setGeneratedQueryComments,
|
||||||
|
} = useQueryCopilot();
|
||||||
|
|
||||||
const sampleProps: SamplePromptsProps = {
|
const sampleProps: SamplePromptsProps = {
|
||||||
isSamplePromptsOpen: isSamplePromptsOpen,
|
isSamplePromptsOpen: isSamplePromptsOpen,
|
||||||
@@ -128,12 +141,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
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[] = [
|
||||||
{ id: 1, text: "Give me all customers whose names start with C" },
|
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
|
||||||
{ id: 2, text: "Show me all customers" },
|
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
|
||||||
{ id: 3, text: "Show me all customers who bought a bike in 2019" },
|
{ 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);
|
||||||
@@ -155,9 +168,16 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateHistories = (): void => {
|
const updateHistories = (): void => {
|
||||||
const newHistories = histories.length < 3 ? [userPrompt, ...histories] : [userPrompt, histories[1], histories[2]];
|
const formattedUserPrompt = userPrompt.replace(/\s+/g, " ").trim();
|
||||||
|
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
|
||||||
|
|
||||||
|
const updatedHistories = existingHistories.filter(
|
||||||
|
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase()
|
||||||
|
);
|
||||||
|
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||||
|
|
||||||
setHistories(newHistories);
|
setHistories(newHistories);
|
||||||
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join(","));
|
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateSQLQuery = async (): Promise<void> => {
|
const generateSQLQuery = async (): Promise<void> => {
|
||||||
@@ -170,23 +190,33 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
userPrompt: userPrompt,
|
userPrompt: userPrompt,
|
||||||
};
|
};
|
||||||
setShowDeletePopup(false);
|
setShowDeletePopup(false);
|
||||||
|
useQueryCopilot.getState().refreshCorrelationId();
|
||||||
const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", {
|
const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||||
if (generateSQLQueryResponse?.sql) {
|
if (response.ok) {
|
||||||
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
if (generateSQLQueryResponse?.sql) {
|
||||||
if (generateSQLQueryResponse.explanation) {
|
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
||||||
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
if (generateSQLQueryResponse.explanation) {
|
||||||
|
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
||||||
|
}
|
||||||
|
query += generateSQLQueryResponse.sql;
|
||||||
|
setQuery(query);
|
||||||
|
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||||
|
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||||
|
setShowErrorMessageBar(false);
|
||||||
}
|
}
|
||||||
query += generateSQLQueryResponse.sql;
|
} else {
|
||||||
setQuery(query);
|
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
|
||||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
useTabs.getState().setIsQueryErrorThrown(true);
|
||||||
|
setShowErrorMessageBar(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "executeNaturalLanguageQuery");
|
handleError(error, "executeNaturalLanguageQuery");
|
||||||
@@ -200,15 +230,14 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
|
|
||||||
options = getCommonQueryOptions(options);
|
|
||||||
return sampleDataClient()
|
|
||||||
.database(QueryCopilotSampleDatabaseId)
|
|
||||||
.container(QueryCopilotSampleContainerId)
|
|
||||||
.items.query(query, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onExecuteQueryClick = async (): Promise<void> => {
|
const onExecuteQueryClick = async (): Promise<void> => {
|
||||||
|
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
|
userPrompt: userPrompt,
|
||||||
|
generatedQuery: generatedQuery,
|
||||||
|
generatedQueryComments: generatedQueryComments,
|
||||||
|
executedQuery: selectedQuery || query,
|
||||||
|
});
|
||||||
const queryToExecute = selectedQuery || query;
|
const queryToExecute = selectedQuery || query;
|
||||||
const queryIterator = querySampleDocuments(queryToExecute, {
|
const queryIterator = querySampleDocuments(queryToExecute, {
|
||||||
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
||||||
@@ -233,8 +262,16 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
|
|
||||||
setQueryResults(queryResults);
|
setQueryResults(queryResults);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
|
setShowErrorMessageBar(false);
|
||||||
|
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
|
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
});
|
||||||
setErrorMessage(errorMessage);
|
setErrorMessage(errorMessage);
|
||||||
handleError(errorMessage, "executeQueryCopilotTab");
|
handleError(errorMessage, "executeQueryCopilotTab");
|
||||||
useTabs.getState().setIsQueryErrorThrown(true);
|
useTabs.getState().setIsQueryErrorThrown(true);
|
||||||
@@ -254,6 +291,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
commandButtonLabel: executeQueryBtnLabel,
|
commandButtonLabel: executeQueryBtnLabel,
|
||||||
ariaLabel: executeQueryBtnLabel,
|
ariaLabel: executeQueryBtnLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
disabled: query?.trim() === "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveQueryBtn = {
|
const saveQueryBtn = {
|
||||||
@@ -264,23 +302,26 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
commandButtonLabel: "Save Query",
|
commandButtonLabel: "Save Query",
|
||||||
ariaLabel: "Save Query",
|
ariaLabel: "Save Query",
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
disabled: query?.trim() === "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const samplePromptsBtn = {
|
// Sample Prompts temporary disabled due current design
|
||||||
iconSrc: SamplePromptsIcon,
|
// const samplePromptsBtn = {
|
||||||
iconAlt: "Sample Prompts",
|
// iconSrc: SamplePromptsIcon,
|
||||||
onCommandClick: () => setIsSamplePromptsOpen(true),
|
// iconAlt: "Sample Prompts",
|
||||||
commandButtonLabel: "Sample Prompts",
|
// onCommandClick: () => setIsSamplePromptsOpen(true),
|
||||||
ariaLabel: "Sample Prompts",
|
// commandButtonLabel: "Sample Prompts",
|
||||||
hasPopup: false,
|
// ariaLabel: "Sample Prompts",
|
||||||
};
|
// hasPopup: false,
|
||||||
|
// };
|
||||||
|
|
||||||
return [executeQueryBtn, saveQueryBtn, samplePromptsBtn];
|
return [executeQueryBtn, saveQueryBtn];
|
||||||
};
|
};
|
||||||
const showTeachingBubble = (): void => {
|
const showTeachingBubble = (): void => {
|
||||||
if (!inputEdited.current) {
|
const shouldShowTeachingBubble = !inputEdited.current && userPrompt.trim() === "";
|
||||||
|
if (shouldShowTeachingBubble) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!inputEdited.current) {
|
if (shouldShowTeachingBubble) {
|
||||||
toggleCopilotTeachingBubbleVisible();
|
toggleCopilotTeachingBubbleVisible();
|
||||||
inputEdited.current = true;
|
inputEdited.current = true;
|
||||||
}
|
}
|
||||||
@@ -294,282 +335,305 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
setShowCallout(false);
|
setShowCallout(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startGenerateQueryProcess = () => {
|
||||||
|
updateHistories();
|
||||||
|
generateSQLQuery();
|
||||||
|
resetButtonState();
|
||||||
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||||
}, [query, selectedQuery]);
|
}, [query, selectedQuery]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (initialInput) {
|
|
||||||
generateSQLQuery();
|
|
||||||
}
|
|
||||||
showTeachingBubble();
|
showTeachingBubble();
|
||||||
useTabs.getState().setIsQueryErrorThrown(false);
|
useTabs.getState().setIsQueryErrorThrown(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="tab-pane" style={{ padding: 24, width: "100%", height: "100%" }}>
|
<Stack className="tab-pane" style={{ padding: 24, width: "100%" }}>
|
||||||
<Stack horizontal verticalAlign="center">
|
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
||||||
<Image src={CopilotIcon} />
|
<Stack horizontal verticalAlign="center">
|
||||||
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
<Image src={CopilotIcon} />
|
||||||
</Stack>
|
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
||||||
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
|
</Stack>
|
||||||
<TextField
|
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
|
||||||
id="naturalLanguageInput"
|
<TextField
|
||||||
value={userPrompt}
|
id="naturalLanguageInput"
|
||||||
onChange={handleUserPromptChange}
|
value={userPrompt}
|
||||||
onClick={() => {
|
onChange={handleUserPromptChange}
|
||||||
inputEdited.current = true;
|
onClick={() => {
|
||||||
setShowSamplePrompts(true);
|
inputEdited.current = true;
|
||||||
}}
|
setShowSamplePrompts(true);
|
||||||
style={{ lineHeight: 30 }}
|
}}
|
||||||
styles={{ root: { width: "95%" } }}
|
onKeyDown={(e) => {
|
||||||
disabled={isGeneratingQuery}
|
if (e.key === "Enter") {
|
||||||
autoComplete="off"
|
startGenerateQueryProcess();
|
||||||
/>
|
}
|
||||||
{copilotTeachingBubbleVisible && (
|
}}
|
||||||
<TeachingBubble
|
style={{ lineHeight: 30 }}
|
||||||
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
styles={{ root: { width: "95%" } }}
|
||||||
target="#naturalLanguageInput"
|
disabled={isGeneratingQuery}
|
||||||
hasCloseButton={true}
|
autoComplete="off"
|
||||||
closeButtonAriaLabel="Close"
|
/>
|
||||||
onDismiss={toggleCopilotTeachingBubbleVisible}
|
{copilotTeachingBubbleVisible && (
|
||||||
hasSmallHeadline={true}
|
<TeachingBubble
|
||||||
headline="Write a prompt"
|
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
||||||
>
|
target="#naturalLanguageInput"
|
||||||
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
hasCloseButton={true}
|
||||||
<Link
|
closeButtonAriaLabel="Close"
|
||||||
onClick={() => {
|
onDismiss={toggleCopilotTeachingBubbleVisible}
|
||||||
setShowSamplePrompts(true);
|
hasSmallHeadline={true}
|
||||||
toggleCopilotTeachingBubbleVisible();
|
headline="Write a prompt"
|
||||||
}}
|
|
||||||
style={{ color: "white", fontWeight: 600 }}
|
|
||||||
>
|
>
|
||||||
sample prompts
|
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
||||||
</Link>{" "}
|
<Link
|
||||||
or write your own query
|
onClick={() => {
|
||||||
</TeachingBubble>
|
setShowSamplePrompts(true);
|
||||||
)}
|
toggleCopilotTeachingBubbleVisible();
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "Send" }}
|
|
||||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
|
||||||
style={{ marginLeft: 8 }}
|
|
||||||
onClick={() => {
|
|
||||||
updateHistories();
|
|
||||||
generateSQLQuery();
|
|
||||||
resetButtonState();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
|
||||||
{showSamplePrompts && (
|
|
||||||
<Callout
|
|
||||||
styles={{ root: { minWidth: 400 } }}
|
|
||||||
target="#naturalLanguageInput"
|
|
||||||
isBeakVisible={false}
|
|
||||||
onDismiss={() => setShowSamplePrompts(false)}
|
|
||||||
directionalHintFixed={true}
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
alignTargetEdge={true}
|
|
||||||
gapSpace={4}
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
{filteredHistories?.length > 0 && (
|
|
||||||
<Stack>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#0078D4",
|
|
||||||
marginLeft: 16,
|
|
||||||
padding: "4px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Recent
|
|
||||||
</Text>
|
|
||||||
{filteredHistories.map((history, i) => (
|
|
||||||
<DefaultButton
|
|
||||||
key={i}
|
|
||||||
onClick={() => {
|
|
||||||
setUserPrompt(history);
|
|
||||||
setShowSamplePrompts(false);
|
|
||||||
}}
|
|
||||||
onRenderIcon={() => <Image src={RecentIcon} />}
|
|
||||||
styles={promptStyles}
|
|
||||||
>
|
|
||||||
{history}
|
|
||||||
</DefaultButton>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#0078D4",
|
|
||||||
marginLeft: 16,
|
|
||||||
padding: "4px 0",
|
|
||||||
}}
|
}}
|
||||||
|
style={{ color: "white", fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
Suggested Prompts
|
sample prompts
|
||||||
</Text>
|
</Link>{" "}
|
||||||
{filteredSuggestedPrompts.map((prompt) => (
|
or write your own query
|
||||||
<DefaultButton
|
</TeachingBubble>
|
||||||
key={prompt.id}
|
|
||||||
onClick={() => {
|
|
||||||
setUserPrompt(prompt.text);
|
|
||||||
setShowSamplePrompts(false);
|
|
||||||
}}
|
|
||||||
onRenderIcon={() => <Image src={HintIcon} />}
|
|
||||||
styles={promptStyles}
|
|
||||||
>
|
|
||||||
{prompt.text}
|
|
||||||
</DefaultButton>
|
|
||||||
))}
|
|
||||||
<Separator
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
selectors: { "::before": { background: "#E1DFDD" } },
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
fontSize: 14,
|
|
||||||
marginLeft: 16,
|
|
||||||
padding: "4px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Learn about{" "}
|
|
||||||
<Link target="_blank" href="">
|
|
||||||
writing effective prompts
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Callout>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Text style={{ marginTop: 8, marginBottom: 24, fontSize: 12 }}>
|
|
||||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
|
||||||
<Link href="" target="_blank">
|
|
||||||
Read preview terms
|
|
||||||
</Link>
|
|
||||||
{showErrorMessageBar && (
|
|
||||||
<Stack style={{ backgroundColor: "#FEF0F1", padding: "4px 8px" }} horizontal verticalAlign="center">
|
|
||||||
<Image src={XErrorMessage} style={{ marginRight: "8px" }} />
|
|
||||||
<Text style={{ fontSize: 12 }}>
|
|
||||||
We ran into an error and were not able to execute query. Please try again after sometime
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{showFeedbackBar && (
|
|
||||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
|
||||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
|
||||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
|
||||||
<Callout
|
|
||||||
style={{ padding: 8 }}
|
|
||||||
target="#likeBtn"
|
|
||||||
onDismiss={() => {
|
|
||||||
setShowCallout(false);
|
|
||||||
submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userPrompt });
|
|
||||||
}}
|
|
||||||
directionalHint={DirectionalHint.topCenter}
|
|
||||||
>
|
|
||||||
<Text>
|
|
||||||
Thank you. Need to give{" "}
|
|
||||||
<Link
|
|
||||||
onClick={() => {
|
|
||||||
setShowCallout(false);
|
|
||||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
more feedback?
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
</Callout>
|
|
||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
id="likeBtn"
|
iconProps={{ iconName: "Send" }}
|
||||||
style={{ marginLeft: 20 }}
|
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
style={{ marginLeft: 8 }}
|
||||||
onClick={() => {
|
onClick={() => startGenerateQueryProcess()}
|
||||||
setShowCallout(!likeQuery);
|
|
||||||
setLikeQuery(!likeQuery);
|
|
||||||
if (dislikeQuery) {
|
|
||||||
setDislikeQuery(!dislikeQuery);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||||
style={{ margin: "0 10px" }}
|
{showSamplePrompts && (
|
||||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
<Callout
|
||||||
onClick={() => {
|
styles={{ root: { minWidth: 400 } }}
|
||||||
if (!dislikeQuery) {
|
target="#naturalLanguageInput"
|
||||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
isBeakVisible={false}
|
||||||
setLikeQuery(false);
|
onDismiss={() => setShowSamplePrompts(false)}
|
||||||
}
|
directionalHintFixed={true}
|
||||||
setDislikeQuery(!dislikeQuery);
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
setShowCallout(false);
|
alignTargetEdge={true}
|
||||||
}}
|
gapSpace={4}
|
||||||
/>
|
>
|
||||||
<Separator vertical style={{ color: "#EDEBE9" }} />
|
<Stack>
|
||||||
<CommandBarButton
|
{filteredHistories?.length > 0 && (
|
||||||
onClick={copyGeneratedCode}
|
<Stack>
|
||||||
iconProps={{ iconName: "Copy" }}
|
<Text
|
||||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
style={{
|
||||||
>
|
width: "100%",
|
||||||
Copy code
|
fontSize: 14,
|
||||||
</CommandBarButton>
|
fontWeight: 600,
|
||||||
<CommandBarButton
|
color: "#0078D4",
|
||||||
onClick={() => {
|
marginLeft: 16,
|
||||||
setShowDeletePopup(true);
|
padding: "4px 0",
|
||||||
}}
|
}}
|
||||||
iconProps={{ iconName: "Delete" }}
|
>
|
||||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
Recent
|
||||||
>
|
</Text>
|
||||||
Delete code
|
{filteredHistories.map((history, i) => (
|
||||||
</CommandBarButton>
|
<DefaultButton
|
||||||
|
key={i}
|
||||||
|
onClick={() => {
|
||||||
|
setUserPrompt(history);
|
||||||
|
setShowSamplePrompts(false);
|
||||||
|
}}
|
||||||
|
onRenderIcon={() => <Image src={RecentIcon} />}
|
||||||
|
styles={promptStyles}
|
||||||
|
>
|
||||||
|
{history}
|
||||||
|
</DefaultButton>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{filteredSuggestedPrompts?.length > 0 && (
|
||||||
|
<Stack>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#0078D4",
|
||||||
|
marginLeft: 16,
|
||||||
|
padding: "4px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Suggested Prompts
|
||||||
|
</Text>
|
||||||
|
{filteredSuggestedPrompts.map((prompt) => (
|
||||||
|
<DefaultButton
|
||||||
|
key={prompt.id}
|
||||||
|
onClick={() => {
|
||||||
|
setUserPrompt(prompt.text);
|
||||||
|
setShowSamplePrompts(false);
|
||||||
|
}}
|
||||||
|
onRenderIcon={() => <Image src={HintIcon} />}
|
||||||
|
styles={promptStyles}
|
||||||
|
>
|
||||||
|
{prompt.text}
|
||||||
|
</DefaultButton>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
|
||||||
|
<Stack>
|
||||||
|
<Separator
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
selectors: { "::before": { background: "#E1DFDD" } },
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
fontSize: 14,
|
||||||
|
marginLeft: 16,
|
||||||
|
padding: "4px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Learn about{" "}
|
||||||
|
<Link target="_blank" href="http://aka.ms/cdb-copilot-writing">
|
||||||
|
writing effective prompts
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Callout>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
|
||||||
|
|
||||||
<Stack className="tabPaneContentContainer">
|
<Stack style={{ marginTop: 8, marginBottom: 24 }}>
|
||||||
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
<Text style={{ fontSize: 12 }}>
|
||||||
<EditorReact
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||||
language={"sql"}
|
<Link href="http://aka.ms/cdb-copilot-preview-terms" target="_blank">
|
||||||
content={query}
|
Read preview terms
|
||||||
isReadOnly={false}
|
</Link>
|
||||||
ariaLabel={"Editing Query"}
|
{showErrorMessageBar && (
|
||||||
lineNumbers={"on"}
|
<Stack style={{ backgroundColor: "#FEF0F1", padding: "4px 8px" }} horizontal verticalAlign="center">
|
||||||
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
<Image src={XErrorMessage} style={{ marginRight: "8px" }} />
|
||||||
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
<Text style={{ fontSize: 12 }}>
|
||||||
|
We ran into an error and were not able to execute query. Please try again after sometime
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{showFeedbackBar && (
|
||||||
|
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
||||||
|
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||||
|
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||||
|
<Callout
|
||||||
|
style={{ padding: 8 }}
|
||||||
|
target="#likeBtn"
|
||||||
|
onDismiss={() => {
|
||||||
|
setShowCallout(false);
|
||||||
|
submitFeedback({
|
||||||
|
generatedQuery: generatedQuery,
|
||||||
|
likeQuery: likeQuery,
|
||||||
|
description: "",
|
||||||
|
userPrompt: userPrompt,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
directionalHint={DirectionalHint.topCenter}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
Thank you. Need to give{" "}
|
||||||
|
<Link
|
||||||
|
onClick={() => {
|
||||||
|
setShowCallout(false);
|
||||||
|
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
more feedback?
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Callout>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
id="likeBtn"
|
||||||
|
style={{ marginLeft: 20 }}
|
||||||
|
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||||
|
onClick={() => {
|
||||||
|
setShowCallout(!likeQuery);
|
||||||
|
setLikeQuery(!likeQuery);
|
||||||
|
if (dislikeQuery) {
|
||||||
|
setDislikeQuery(!dislikeQuery);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
style={{ margin: "0 10px" }}
|
||||||
|
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||||
|
onClick={() => {
|
||||||
|
if (!dislikeQuery) {
|
||||||
|
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
||||||
|
setLikeQuery(false);
|
||||||
|
}
|
||||||
|
setDislikeQuery(!dislikeQuery);
|
||||||
|
setShowCallout(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Separator vertical style={{ color: "#EDEBE9" }} />
|
||||||
|
<CommandBarButton
|
||||||
|
onClick={copyGeneratedCode}
|
||||||
|
iconProps={{ iconName: "Copy" }}
|
||||||
|
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||||
|
>
|
||||||
|
Copy code
|
||||||
|
</CommandBarButton>
|
||||||
|
<CommandBarButton
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeletePopup(true);
|
||||||
|
}}
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||||
|
>
|
||||||
|
Delete code
|
||||||
|
</CommandBarButton>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack className="tabPaneContentContainer">
|
||||||
|
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
||||||
|
<EditorReact
|
||||||
|
language={"sql"}
|
||||||
|
content={query}
|
||||||
|
isReadOnly={false}
|
||||||
|
ariaLabel={"Editing Query"}
|
||||||
|
lineNumbers={"on"}
|
||||||
|
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
||||||
|
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
||||||
|
/>
|
||||||
|
<QueryResultSection
|
||||||
|
isMongoDB={false}
|
||||||
|
queryEditorContent={selectedQuery || query}
|
||||||
|
error={errorMessage}
|
||||||
|
queryResults={queryResults}
|
||||||
|
isExecuting={isExecuting}
|
||||||
|
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||||
|
queryDocumentsPerPage(firstItemIndex, queryIterator)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SplitterLayout>
|
||||||
|
</Stack>
|
||||||
|
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
||||||
|
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||||
|
{query !== "" && query.trim().length !== 0 && (
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={showDeletePopup}
|
||||||
|
setShowDeletePopup={setShowDeletePopup}
|
||||||
|
setQuery={setQuery}
|
||||||
|
clearFeedback={resetButtonState}
|
||||||
|
showFeedbackBar={setShowFeedbackBar}
|
||||||
/>
|
/>
|
||||||
<QueryResultSection
|
)}
|
||||||
isMongoDB={false}
|
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
||||||
queryEditorContent={selectedQuery || query}
|
</div>
|
||||||
error={errorMessage}
|
|
||||||
queryResults={queryResults}
|
|
||||||
isExecuting={isExecuting}
|
|
||||||
executeQueryDocumentsPage={(firstItemIndex: number) => queryDocumentsPerPage(firstItemIndex, queryIterator)}
|
|
||||||
/>
|
|
||||||
</SplitterLayout>
|
|
||||||
</Stack>
|
|
||||||
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
|
||||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
|
||||||
{query !== "" && query.trim().length !== 0 && (
|
|
||||||
<DeletePopup
|
|
||||||
showDeletePopup={showDeletePopup}
|
|
||||||
setShowDeletePopup={setShowDeletePopup}
|
|
||||||
setQuery={setQuery}
|
|
||||||
clearFeedback={resetButtonState}
|
|
||||||
showFeedbackBar={setShowFeedbackBar}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
225
src/Explorer/QueryCopilot/QueryCopilotUtilities.test.tsx
Normal file
225
src/Explorer/QueryCopilot/QueryCopilotUtilities.test.tsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
|
import { QueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { sampleDataClient } from "Common/SampleDataClient";
|
||||||
|
import * as commonUtils from "Common/dataAccess/queryDocuments";
|
||||||
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import { querySampleDocuments, readSampleDocument, submitFeedback } from "./QueryCopilotUtilities";
|
||||||
|
jest.mock("Explorer/Tree/DocumentId", () => {
|
||||||
|
return jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
id: jest.fn(),
|
||||||
|
loadDocument: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("Utils/NotificationConsoleUtils", () => ({
|
||||||
|
logConsoleProgress: jest.fn(),
|
||||||
|
logConsoleError: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@azure/cosmos", () => ({
|
||||||
|
FeedOptions: jest.fn(),
|
||||||
|
QueryIterator: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/ErrorHandlingUtils", () => ({
|
||||||
|
handleError: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Utils/NotificationConsoleUtils", () => ({
|
||||||
|
logConsoleProgress: jest.fn().mockReturnValue((): void => undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/dataAccess/queryDocuments", () => ({
|
||||||
|
getCommonQueryOptions: jest.fn((options) => options),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/SampleDataClient");
|
||||||
|
|
||||||
|
jest.mock("node-fetch");
|
||||||
|
|
||||||
|
describe("QueryCopilotUtilities", () => {
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
describe("submitFeedback", () => {
|
||||||
|
const payload = {
|
||||||
|
like: "like",
|
||||||
|
generatedSql: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: "Description",
|
||||||
|
contact: "Contact",
|
||||||
|
containerSchema: QueryCopilotSampleContainerSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should call fetch with the payload with like", async () => {
|
||||||
|
const mockFetch = jest.fn().mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
useQueryCopilot.getState().refreshCorrelationId();
|
||||||
|
|
||||||
|
await submitFeedback({
|
||||||
|
likeQuery: true,
|
||||||
|
generatedQuery: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: "Description",
|
||||||
|
contact: "Contact",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
"https://copilotorchestrater.azurewebsites.net/feedback",
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||||
|
expect(actualBody).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call fetch with the payload with unlike and empty parameters", async () => {
|
||||||
|
payload.like = "dislike";
|
||||||
|
payload.description = "";
|
||||||
|
payload.contact = "";
|
||||||
|
const mockFetch = jest.fn().mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
useQueryCopilot.getState().refreshCorrelationId();
|
||||||
|
|
||||||
|
await submitFeedback({
|
||||||
|
likeQuery: false,
|
||||||
|
generatedQuery: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: undefined,
|
||||||
|
contact: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
"https://copilotorchestrater.azurewebsites.net/feedback",
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||||
|
expect(actualBody).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors and call handleError", async () => {
|
||||||
|
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
|
||||||
|
|
||||||
|
await submitFeedback({
|
||||||
|
likeQuery: true,
|
||||||
|
generatedQuery: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: "Description",
|
||||||
|
contact: "Contact",
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error.message).toEqual("Mock error");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleError).toHaveBeenCalledWith(new Error("Mock error"), expect.any(String));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("querySampleDocuments", () => {
|
||||||
|
(sampleDataClient as jest.Mock).mockReturnValue({
|
||||||
|
database: jest.fn().mockReturnValue({
|
||||||
|
container: jest.fn().mockReturnValue({
|
||||||
|
items: {
|
||||||
|
query: jest.fn().mockReturnValue([]),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls getCommonQueryOptions with the provided options", () => {
|
||||||
|
const query = "sample query";
|
||||||
|
const options: FeedOptions = { maxItemCount: 10 };
|
||||||
|
|
||||||
|
querySampleDocuments(query, options);
|
||||||
|
|
||||||
|
expect(commonUtils.getCommonQueryOptions).toHaveBeenCalledWith(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the result of items.query method", () => {
|
||||||
|
const query = "sample query";
|
||||||
|
const options: FeedOptions = { maxItemCount: 10 };
|
||||||
|
const mockResult = [
|
||||||
|
{ id: 1, name: "Document 1" },
|
||||||
|
{ id: 2, name: "Document 2" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock the items.query method to return the mockResult
|
||||||
|
(sampleDataClient().database("CopilotSampleDb").container("SampleContainer").items
|
||||||
|
.query as jest.Mock).mockReturnValue(mockResult);
|
||||||
|
|
||||||
|
const result = querySampleDocuments(query, options);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("readSampleDocument", () => {
|
||||||
|
it("should call the read method with the correct parameters", async () => {
|
||||||
|
(sampleDataClient as jest.Mock).mockReturnValue({
|
||||||
|
database: jest.fn().mockReturnValue({
|
||||||
|
container: jest.fn().mockReturnValue({
|
||||||
|
item: jest.fn().mockReturnValue({
|
||||||
|
read: jest.fn().mockResolvedValue({
|
||||||
|
resource: {},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentId = new DocumentId(null, "DocumentId", []);
|
||||||
|
const expectedResponse = {};
|
||||||
|
|
||||||
|
const result = await readSampleDocument(documentId);
|
||||||
|
|
||||||
|
expect(sampleDataClient).toHaveBeenCalled();
|
||||||
|
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
|
||||||
|
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
|
||||||
|
expect(
|
||||||
|
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(expectedResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle an error and re-throw it", async () => {
|
||||||
|
(sampleDataClient as jest.Mock).mockReturnValue({
|
||||||
|
database: jest.fn().mockReturnValue({
|
||||||
|
container: jest.fn().mockReturnValue({
|
||||||
|
item: jest.fn().mockReturnValue({
|
||||||
|
read: jest.fn().mockRejectedValue(new Error("Mock error")),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorMock = new Error("Mock error");
|
||||||
|
const documentId = new DocumentId(null, "DocumentId", []);
|
||||||
|
|
||||||
|
await expect(readSampleDocument(documentId)).rejects.toStrictEqual(errorMock);
|
||||||
|
|
||||||
|
expect(sampleDataClient).toHaveBeenCalled();
|
||||||
|
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
|
||||||
|
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
|
||||||
|
expect(
|
||||||
|
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(handleError).toHaveBeenCalledWith(errorMock, "ReadDocument", expect.any(String));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
import { QueryCopilotSampleContainerSchema } from "Common/Constants";
|
import { FeedOptions, Item, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
|
import {
|
||||||
|
QueryCopilotSampleContainerId,
|
||||||
|
QueryCopilotSampleContainerSchema,
|
||||||
|
QueryCopilotSampleDatabaseId,
|
||||||
|
} from "Common/Constants";
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { sampleDataClient } from "Common/SampleDataClient";
|
||||||
|
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
||||||
|
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
||||||
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
|
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
|
||||||
interface FeedbackParams {
|
interface FeedbackParams {
|
||||||
likeQuery: boolean;
|
likeQuery: boolean;
|
||||||
@@ -21,16 +32,42 @@ export const submitFeedback = async (params: FeedbackParams): Promise<void> => {
|
|||||||
contact: contact || "",
|
contact: contact || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
|
await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "copilotSubmitFeedback");
|
handleError(error, "copilotSubmitFeedback");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
|
||||||
|
options = getCommonQueryOptions(options);
|
||||||
|
return sampleDataClient()
|
||||||
|
.database(QueryCopilotSampleDatabaseId)
|
||||||
|
.container(QueryCopilotSampleContainerId)
|
||||||
|
.items.query(query, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readSampleDocument = async (documentId: DocumentId): Promise<Item> => {
|
||||||
|
const clearMessage = logConsoleProgress(`Reading item ${documentId.id()}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await sampleDataClient()
|
||||||
|
.database(QueryCopilotSampleDatabaseId)
|
||||||
|
.container(QueryCopilotSampleContainerId)
|
||||||
|
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||||
|
.read();
|
||||||
|
|
||||||
|
return response?.resource;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "ReadDocument", `Failed to read item ${documentId.id()}`);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,26 +1,91 @@
|
|||||||
|
import { DefaultButton, IconButton } from "@fluentui/react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SamplePrompts, SamplePromptsProps } from "./SamplePrompts";
|
import { SamplePrompts, SamplePromptsProps } from "./SamplePrompts";
|
||||||
|
|
||||||
describe("Sample Prompts snapshot test", () => {
|
describe("Sample Prompts snapshot test", () => {
|
||||||
it("should render properly if isSamplePromptsOpen is true", () => {
|
const setTextBoxMock = jest.fn();
|
||||||
const sampleProps: SamplePromptsProps = {
|
const setIsSamplePromptsOpenMock = jest.fn();
|
||||||
isSamplePromptsOpen: true,
|
const sampleProps: SamplePromptsProps = {
|
||||||
setIsSamplePromptsOpen: () => undefined,
|
isSamplePromptsOpen: true,
|
||||||
setTextBox: () => undefined,
|
setIsSamplePromptsOpen: setIsSamplePromptsOpenMock,
|
||||||
};
|
setTextBox: setTextBoxMock,
|
||||||
|
};
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
it("should render properly if isSamplePromptsOpen is true", () => {
|
||||||
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render properly if isSamplePromptsOpen is false", () => {
|
it("should render properly if isSamplePromptsOpen is false", () => {
|
||||||
const sampleProps: SamplePromptsProps = {
|
sampleProps.isSamplePromptsOpen = false;
|
||||||
isSamplePromptsOpen: false,
|
|
||||||
setIsSamplePromptsOpen: () => undefined,
|
|
||||||
setTextBox: () => undefined,
|
|
||||||
};
|
|
||||||
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should call setTextBox and setIsSamplePromptsOpen(false) when a button is clicked", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(0).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith("Show me products less than 100 dolars");
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(3).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith(
|
||||||
|
"Write a query to return all records in this table created in the last thirty days"
|
||||||
|
);
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setIsSamplePromptsOpen(false) when the close button is clicked", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
wrapper.find(IconButton).first().simulate("click");
|
||||||
|
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setTextBox and setIsSamplePromptsOpen(false) when a simple prompt button is clicked", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(0).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith("Show me products less than 100 dolars");
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(1).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith("Show schema");
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setTextBox and setIsSamplePromptsOpen(false) when an intermediate prompt button is clicked", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(2).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith(
|
||||||
|
"Show items with a description that contains a number between 0 and 99 inclusive."
|
||||||
|
);
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(3).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith(
|
||||||
|
"Write a query to return all records in this table created in the last thirty days"
|
||||||
|
);
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setTextBox and setIsSamplePromptsOpen(false) when a complex prompt button is clicked", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(4).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith("Show all the products that customer Bob has reviewed.");
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(5).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith("Which computers are more than 300 dollars and less than 400 dollars?");
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,133 +5,149 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
className="tab-pane"
|
className="tab-pane"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"height": "100%",
|
|
||||||
"padding": 24,
|
"padding": 24,
|
||||||
"width": "100%",
|
"width": "100%",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Stack
|
<div
|
||||||
horizontal={true}
|
|
||||||
verticalAlign="center"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src=""
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"fontSize": 16,
|
|
||||||
"fontWeight": 600,
|
|
||||||
"marginLeft": 8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Copilot
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"marginTop": 16,
|
"height": "100%",
|
||||||
"position": "relative",
|
"overflowY": "auto",
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verticalAlign="center"
|
|
||||||
>
|
|
||||||
<StyledTextFieldBase
|
|
||||||
autoComplete="off"
|
|
||||||
disabled={false}
|
|
||||||
id="naturalLanguageInput"
|
|
||||||
onChange={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"lineHeight": 30,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"width": "95%",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="Write a query to return all records in this table"
|
|
||||||
/>
|
|
||||||
<CustomizedIconButton
|
|
||||||
disabled={false}
|
|
||||||
iconProps={
|
|
||||||
Object {
|
|
||||||
"iconName": "Send",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginLeft": 8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Text
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"fontSize": 12,
|
|
||||||
"marginBottom": 24,
|
|
||||||
"marginTop": 8,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
<StyledLinkBase
|
verticalAlign="center"
|
||||||
href=""
|
|
||||||
target="_blank"
|
|
||||||
>
|
>
|
||||||
Read preview terms
|
<Image
|
||||||
</StyledLinkBase>
|
src=""
|
||||||
</Text>
|
/>
|
||||||
<Stack
|
<Text
|
||||||
className="tabPaneContentContainer"
|
style={
|
||||||
>
|
Object {
|
||||||
<t
|
"fontSize": 16,
|
||||||
customClassName=""
|
"fontWeight": 600,
|
||||||
onDragEnd={null}
|
"marginLeft": 8,
|
||||||
onDragStart={null}
|
}
|
||||||
onSecondaryPaneSizeChange={null}
|
}
|
||||||
percentage={false}
|
>
|
||||||
primaryIndex={0}
|
Copilot
|
||||||
primaryMinSize={100}
|
</Text>
|
||||||
secondaryMinSize={200}
|
</Stack>
|
||||||
vertical={true}
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginTop": 16,
|
||||||
|
"position": "relative",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
>
|
>
|
||||||
<EditorReact
|
<StyledTextFieldBase
|
||||||
ariaLabel="Editing Query"
|
autoComplete="off"
|
||||||
content=""
|
disabled={false}
|
||||||
isReadOnly={false}
|
id="naturalLanguageInput"
|
||||||
language="sql"
|
onChange={[Function]}
|
||||||
lineNumbers="on"
|
onClick={[Function]}
|
||||||
onContentChanged={[Function]}
|
onKeyDown={[Function]}
|
||||||
onContentSelected={[Function]}
|
style={
|
||||||
|
Object {
|
||||||
|
"lineHeight": 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": "95%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
/>
|
/>
|
||||||
<QueryResultSection
|
<CustomizedIconButton
|
||||||
error=""
|
disabled={true}
|
||||||
executeQueryDocumentsPage={[Function]}
|
iconProps={
|
||||||
isExecuting={false}
|
Object {
|
||||||
isMongoDB={false}
|
"iconName": "Send",
|
||||||
queryEditorContent=""
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginLeft": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</t>
|
</Stack>
|
||||||
</Stack>
|
<Stack
|
||||||
<WelcomeModal
|
style={
|
||||||
visible={true}
|
Object {
|
||||||
/>
|
"marginBottom": 24,
|
||||||
<CopyPopup
|
"marginTop": 8,
|
||||||
setShowCopyPopup={[Function]}
|
}
|
||||||
showCopyPopup={false}
|
}
|
||||||
/>
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href="http://aka.ms/cdb-copilot-preview-terms"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Read preview terms
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
className="tabPaneContentContainer"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
customClassName=""
|
||||||
|
onDragEnd={null}
|
||||||
|
onDragStart={null}
|
||||||
|
onSecondaryPaneSizeChange={null}
|
||||||
|
percentage={false}
|
||||||
|
primaryIndex={0}
|
||||||
|
primaryMinSize={100}
|
||||||
|
secondaryMinSize={200}
|
||||||
|
vertical={true}
|
||||||
|
>
|
||||||
|
<EditorReact
|
||||||
|
ariaLabel="Editing Query"
|
||||||
|
content=""
|
||||||
|
isReadOnly={false}
|
||||||
|
language="sql"
|
||||||
|
lineNumbers="on"
|
||||||
|
onContentChanged={[Function]}
|
||||||
|
onContentSelected={[Function]}
|
||||||
|
/>
|
||||||
|
<QueryResultSection
|
||||||
|
error=""
|
||||||
|
executeQueryDocumentsPage={[Function]}
|
||||||
|
isExecuting={false}
|
||||||
|
isMongoDB={false}
|
||||||
|
queryEditorContent=""
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
</Stack>
|
||||||
|
<WelcomeModal
|
||||||
|
visible={true}
|
||||||
|
/>
|
||||||
|
<CopyPopup
|
||||||
|
setShowCopyPopup={[Function]}
|
||||||
|
showCopyPopup={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
() => this.setState({}),
|
() => this.setState({}),
|
||||||
(state) => state.showResetPasswordBubble
|
(state) => state.showResetPasswordBubble
|
||||||
),
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dispose: useDatabases.subscribe(
|
||||||
|
() => this.setState({}),
|
||||||
|
(state) => state.sampleDataResourceTokenCollection
|
||||||
|
),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -107,7 +113,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getSplashScreenButtons = (): JSX.Element => {
|
private getSplashScreenButtons = (): JSX.Element => {
|
||||||
if (userContext.features.enableCopilot && userContext.apiType === "SQL") {
|
if (
|
||||||
|
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||||
|
userContext.features.enableCopilot &&
|
||||||
|
userContext.apiType === "SQL"
|
||||||
|
) {
|
||||||
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 }}>
|
||||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||||
@@ -137,7 +147,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
description={
|
description={
|
||||||
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
||||||
}
|
}
|
||||||
onClick={() => useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot)}
|
onClick={() => {
|
||||||
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||||
|
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<SplashScreenButton
|
<SplashScreenButton
|
||||||
imgSrc={ConnectIcon}
|
imgSrc={ConnectIcon}
|
||||||
@@ -246,8 +259,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
<form className="connectExplorerFormContainer">
|
<form className="connectExplorerFormContainer">
|
||||||
<div className="splashScreenContainer">
|
<div className="splashScreenContainer">
|
||||||
<div className="splashScreen">
|
<div className="splashScreen">
|
||||||
<div
|
<h1
|
||||||
className="title"
|
className="title"
|
||||||
|
role="heading"
|
||||||
aria-label={
|
aria-label={
|
||||||
userContext.apiType === "Postgres"
|
userContext.apiType === "Postgres"
|
||||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||||
@@ -258,7 +272,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||||
: "Welcome to Azure Cosmos DB"}
|
: "Welcome to Azure Cosmos DB"}
|
||||||
<FeaturePanelLauncher />
|
<FeaturePanelLauncher />
|
||||||
</div>
|
</h1>
|
||||||
<div className="subtitle">
|
<div className="subtitle">
|
||||||
{userContext.apiType === "Postgres"
|
{userContext.apiType === "Postgres"
|
||||||
? "Get started with our sample datasets, documentation, and additional tools."
|
? "Get started with our sample datasets, documentation, and additional tools."
|
||||||
@@ -581,7 +595,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Image src={LinkIcon} alt=" " />
|
<Image src={LinkIcon} alt={item.title} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text>{item.description}</Text>
|
<Text>{item.description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -600,7 +614,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
<li key={`${item.title}${item.description}${index}`}>
|
<li key={`${item.title}${item.description}${index}`}>
|
||||||
<Stack style={{ marginBottom: 26 }}>
|
<Stack style={{ marginBottom: 26 }}>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Image style={{ marginRight: 8 }} src={item.iconSrc} />
|
<Image style={{ marginRight: 8 }} src={item.iconSrc} alt={item.title} />
|
||||||
<Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info}>
|
<Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -720,7 +734,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Image src={LinkIcon} />
|
<Image src={LinkIcon} alt={item.title} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text>{item.description}</Text>
|
<Text>{item.description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
||||||
@@ -7,7 +8,12 @@ import NewDocumentIcon from "../../../images/NewDocument.svg";
|
|||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
import UploadIcon from "../../../images/Upload_16x16.svg";
|
import UploadIcon from "../../../images/Upload_16x16.svg";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
|
import {
|
||||||
|
DocumentsGridMetrics,
|
||||||
|
KeyCodes,
|
||||||
|
QueryCopilotSampleContainerId,
|
||||||
|
QueryCopilotSampleDatabaseId,
|
||||||
|
} from "../../Common/Constants";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||||
@@ -71,6 +77,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
|
|
||||||
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
|
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
|
||||||
private _resourceTokenPartitionKey: string;
|
private _resourceTokenPartitionKey: string;
|
||||||
|
private _isQueryCopilotSampleContainer: boolean;
|
||||||
|
|
||||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -317,6 +324,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
|
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
|
||||||
|
|
||||||
this.showPartitionKey = this._shouldShowPartitionKey();
|
this.showPartitionKey = this._shouldShowPartitionKey();
|
||||||
|
this._isQueryCopilotSampleContainer =
|
||||||
|
this.collection?.databaseId === QueryCopilotSampleDatabaseId &&
|
||||||
|
this.collection?.id() === QueryCopilotSampleContainerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _shouldShowPartitionKey(): boolean {
|
private _shouldShowPartitionKey(): boolean {
|
||||||
@@ -678,7 +688,6 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public createIterator(): QueryIterator<ItemDefinition & Resource> {
|
public createIterator(): QueryIterator<ItemDefinition & Resource> {
|
||||||
let filters = this.lastFilterContents();
|
|
||||||
const filter: string = this.filterContent().trim();
|
const filter: string = this.filterContent().trim();
|
||||||
const query: string = this.buildQuery(filter);
|
const query: string = this.buildQuery(filter);
|
||||||
let options: any = {};
|
let options: any = {};
|
||||||
@@ -688,12 +697,16 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
options.partitionKey = this._resourceTokenPartitionKey;
|
options.partitionKey = this._resourceTokenPartitionKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
|
return this._isQueryCopilotSampleContainer
|
||||||
|
? querySampleDocuments(query, options)
|
||||||
|
: queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async selectDocument(documentId: DocumentId): Promise<void> {
|
public async selectDocument(documentId: DocumentId): Promise<void> {
|
||||||
this.selectedDocumentId(documentId);
|
this.selectedDocumentId(documentId);
|
||||||
const content = await readDocument(this.collection, documentId);
|
const content = await (this._isQueryCopilotSampleContainer
|
||||||
|
? readSampleDocument(documentId)
|
||||||
|
: readDocument(this.collection, documentId));
|
||||||
this.initDocumentEditor(documentId, content);
|
this.initDocumentEditor(documentId, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,7 +823,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.newDocumentButton.enabled(),
|
disabled: !this.newDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
id: "mongoNewDocumentBtn",
|
id: "mongoNewDocumentBtn",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -824,7 +837,8 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.saveNewDocumentButton.enabled(),
|
disabled:
|
||||||
|
!this.saveNewDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,7 +851,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.discardNewDocumentChangesButton.enabled(),
|
disabled:
|
||||||
|
!this.discardNewDocumentChangesButton.enabled() ||
|
||||||
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,7 +866,8 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.saveExistingDocumentButton.enabled(),
|
disabled:
|
||||||
|
!this.saveExistingDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -863,7 +880,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.discardExisitingDocumentChangesButton.enabled(),
|
disabled:
|
||||||
|
!this.discardExisitingDocumentChangesButton.enabled() ||
|
||||||
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,7 +895,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.deleteExisitingDocumentButton.enabled(),
|
disabled:
|
||||||
|
!this.deleteExisitingDocumentButton.enabled() ||
|
||||||
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,7 +941,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
|
disabled:
|
||||||
|
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
|
||||||
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
|||||||
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||||
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||||
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";
|
||||||
@@ -68,7 +69,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?: ReactTabKind }) {
|
function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?: ReactTabKind }) {
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||||
const tabId = tab ? tab.tabId : "connect";
|
const tabId = tab ? tab.tabId : "";
|
||||||
|
|
||||||
const getReactTabTitle = (): ko.Observable<string> => {
|
const getReactTabTitle = (): ko.Observable<string> => {
|
||||||
if (tabKind === ReactTabKind.QueryCopilot) {
|
if (tabKind === ReactTabKind.QueryCopilot) {
|
||||||
@@ -158,6 +159,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();
|
||||||
}}
|
}}
|
||||||
tabIndex={active ? 0 : undefined}
|
tabIndex={active ? 0 : undefined}
|
||||||
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
|
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
|
||||||
@@ -251,7 +253,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
case ReactTabKind.Quickstart:
|
case ReactTabKind.Quickstart:
|
||||||
return <QuickstartTab explorer={explorer} />;
|
return <QuickstartTab explorer={explorer} />;
|
||||||
case ReactTabKind.QueryCopilot:
|
case ReactTabKind.QueryCopilot:
|
||||||
return <QueryCopilotTab initialInput={useTabs.getState().queryCopilotTabInitialInput} explorer={explorer} />;
|
return <QueryCopilotTab explorer={explorer} />;
|
||||||
default:
|
default:
|
||||||
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import * as ko from "knockout";
|
|||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
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 { 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 { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
import DocumentsTab from "../Tabs/DocumentsTab";
|
||||||
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
|
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
|
||||||
@@ -28,8 +28,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
|||||||
public children: ko.ObservableArray<ViewModels.TreeNode>;
|
public children: ko.ObservableArray<ViewModels.TreeNode>;
|
||||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||||
public isCollectionExpanded: ko.Observable<boolean>;
|
public isCollectionExpanded: ko.Observable<boolean>;
|
||||||
|
public isSampleCollection?: boolean;
|
||||||
|
|
||||||
constructor(container: Explorer, databaseId: string, data: DataModels.Collection) {
|
constructor(container: Explorer, databaseId: string, data: DataModels.Collection, isSampleCollection?: boolean) {
|
||||||
this.nodeKind = "Collection";
|
this.nodeKind = "Collection";
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.databaseId = databaseId;
|
this.databaseId = databaseId;
|
||||||
@@ -42,6 +43,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
|||||||
this.children = ko.observableArray<ViewModels.TreeNode>([]);
|
this.children = ko.observableArray<ViewModels.TreeNode>([]);
|
||||||
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
||||||
this.isCollectionExpanded = ko.observable<boolean>(true);
|
this.isCollectionExpanded = ko.observable<boolean>(true);
|
||||||
|
this.isSampleCollection = isSampleCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public expandCollection(): void {
|
public expandCollection(): void {
|
||||||
@@ -139,7 +141,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
|||||||
|
|
||||||
documentsTab = new DocumentsTab({
|
documentsTab = new DocumentsTab({
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
resourceTokenPartitionKey: userContext.parsedResourceToken.partitionKey,
|
resourceTokenPartitionKey: userContext.parsedResourceToken?.partitionKey,
|
||||||
documentIds: ko.observableArray<DocumentId>([]),
|
documentIds: ko.observableArray<DocumentId>([]),
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||||
title: "Items",
|
title: "Items",
|
||||||
|
|||||||
@@ -790,14 +790,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
{!isNotebookEnabled && isSampleDataEnabled && (
|
{!isNotebookEnabled && isSampleDataEnabled && (
|
||||||
<>
|
<>
|
||||||
<AccordionComponent>
|
<AccordionComponent>
|
||||||
<AccordionItemComponent
|
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||||
title={"MY DATA"}
|
|
||||||
isExpanded={!gitHubNotebooksContentRoot}
|
|
||||||
styles={{ maxHeight: 230 }}
|
|
||||||
>
|
|
||||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
<AccordionItemComponent title={"SAMPLE DATA"}>
|
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
||||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
</AccordionComponent>
|
</AccordionComponent>
|
||||||
@@ -808,14 +804,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
{isNotebookEnabled && isSampleDataEnabled && (
|
{isNotebookEnabled && isSampleDataEnabled && (
|
||||||
<>
|
<>
|
||||||
<AccordionComponent>
|
<AccordionComponent>
|
||||||
<AccordionItemComponent
|
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||||
title={"MY DATA"}
|
|
||||||
isExpanded={!gitHubNotebooksContentRoot}
|
|
||||||
styles={{ maxHeight: 130 }}
|
|
||||||
>
|
|
||||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
<AccordionItemComponent title={"SAMPLE DATA"}>
|
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
||||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdap
|
|||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -14,51 +14,65 @@ export const SampleDataTree = ({
|
|||||||
}: {
|
}: {
|
||||||
sampleDataResourceTokenCollection: ViewModels.CollectionBase;
|
sampleDataResourceTokenCollection: ViewModels.CollectionBase;
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const [root, setRoot] = useState<TreeNode | undefined>(undefined);
|
const buildSampleDataTree = (): TreeNode => {
|
||||||
|
const updatedSampleTree: TreeNode = {
|
||||||
useEffect(() => {
|
label: sampleDataResourceTokenCollection.databaseId,
|
||||||
if (sampleDataResourceTokenCollection) {
|
isExpanded: false,
|
||||||
const updatedSampleTree: TreeNode = {
|
iconSrc: CosmosDBIcon,
|
||||||
label: sampleDataResourceTokenCollection.databaseId,
|
className: "databaseHeader",
|
||||||
isExpanded: false,
|
children: [
|
||||||
iconSrc: CosmosDBIcon,
|
{
|
||||||
className: "databaseHeader",
|
label: sampleDataResourceTokenCollection.id(),
|
||||||
children: [
|
iconSrc: CollectionIcon,
|
||||||
{
|
isExpanded: false,
|
||||||
label: sampleDataResourceTokenCollection.id(),
|
className: "collectionHeader",
|
||||||
iconSrc: CollectionIcon,
|
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
||||||
isExpanded: false,
|
onClick: () => {
|
||||||
className: "dataResourceTree",
|
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
useCommandBar.getState().setContextButtons([]);
|
||||||
onClick: () => {
|
useTabs
|
||||||
// Rewritten version of expandCollapseCollection
|
.getState()
|
||||||
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
|
.refreshActiveTab(
|
||||||
useCommandBar.getState().setContextButtons([]);
|
|
||||||
useTabs().refreshActiveTab(
|
|
||||||
(tab: TabsBase) =>
|
(tab: TabsBase) =>
|
||||||
tab.collection?.id() === sampleDataResourceTokenCollection.id() &&
|
tab.collection?.id() === sampleDataResourceTokenCollection.id() &&
|
||||||
tab.collection.databaseId === sampleDataResourceTokenCollection.databaseId
|
tab.collection.databaseId === sampleDataResourceTokenCollection.databaseId
|
||||||
);
|
);
|
||||||
},
|
|
||||||
isSelected: () =>
|
|
||||||
useSelectedNode
|
|
||||||
.getState()
|
|
||||||
.isDataNodeSelected(
|
|
||||||
sampleDataResourceTokenCollection.databaseId,
|
|
||||||
sampleDataResourceTokenCollection.id()
|
|
||||||
),
|
|
||||||
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: "Items",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
isSelected: () =>
|
||||||
};
|
useSelectedNode
|
||||||
setRoot(updatedSampleTree);
|
.getState()
|
||||||
}
|
.isDataNodeSelected(sampleDataResourceTokenCollection.databaseId, sampleDataResourceTokenCollection.id()),
|
||||||
}, [sampleDataResourceTokenCollection]);
|
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: "Items",
|
||||||
|
onClick: () => sampleDataResourceTokenCollection.onDocumentDBDocumentsClick(),
|
||||||
|
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
||||||
|
isSelected: () =>
|
||||||
|
useSelectedNode
|
||||||
|
.getState()
|
||||||
|
.isDataNodeSelected(
|
||||||
|
sampleDataResourceTokenCollection.databaseId,
|
||||||
|
sampleDataResourceTokenCollection.id(),
|
||||||
|
[ViewModels.CollectionTabKind.Documents]
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return <TreeComponent className="dataResourceTree" rootNode={root || { label: "Sample data not initialized." }} />;
|
return {
|
||||||
|
label: undefined,
|
||||||
|
isExpanded: true,
|
||||||
|
children: [updatedSampleTree],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeComponent
|
||||||
|
className="dataResourceTree"
|
||||||
|
rootNode={sampleDataResourceTokenCollection ? buildSampleDataTree() : { label: "Sample data not initialized." }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,11 +66,12 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
|
|||||||
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
|
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
|
||||||
},
|
},
|
||||||
isQueryCopilotCollectionSelected: (): boolean => {
|
isQueryCopilotCollectionSelected: (): boolean => {
|
||||||
const selectedNode = get().selectedNode;
|
const selectedNode = get().selectedNode as ViewModels.CollectionBase;
|
||||||
if (
|
if (
|
||||||
selectedNode &&
|
selectedNode &&
|
||||||
|
selectedNode.isSampleCollection &&
|
||||||
selectedNode.id() === QueryCopilotSampleContainerId &&
|
selectedNode.id() === QueryCopilotSampleContainerId &&
|
||||||
(selectedNode as ViewModels.Collection)?.databaseId === QueryCopilotSampleDatabaseId
|
selectedNode.databaseId === QueryCopilotSampleDatabaseId
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import "../externals/jquery.typeahead.min.css";
|
|||||||
import "../externals/jquery.typeahead.min.js";
|
import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export type Features = {
|
|||||||
readonly enableLegacyMongoShellV2Debug: boolean;
|
readonly enableLegacyMongoShellV2Debug: boolean;
|
||||||
readonly loadLegacyMongoShellFromBE: boolean;
|
readonly loadLegacyMongoShellFromBE: boolean;
|
||||||
readonly enableCopilot: boolean;
|
readonly enableCopilot: boolean;
|
||||||
|
readonly enableNPSSurvey: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -104,6 +105,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"),
|
enableCopilot: "true" === get("enablecopilot"),
|
||||||
|
enableNPSSurvey: "true" === get("enablenpssurvey"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ export enum Action {
|
|||||||
LaunchUITour,
|
LaunchUITour,
|
||||||
CancelUITour,
|
CancelUITour,
|
||||||
CompleteUITour,
|
CompleteUITour,
|
||||||
|
OpenQueryCopilotFromSplashScreen,
|
||||||
|
OpenQueryCopilotFromNewQuery,
|
||||||
|
ExecuteQueryGeneratedFromQueryCopilot,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionModifiers = {
|
export const ActionModifiers = {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const userContext: UserContext = {
|
|||||||
collectionCreationDefaults: CollectionCreationDefaults,
|
collectionCreationDefaults: CollectionCreationDefaults,
|
||||||
};
|
};
|
||||||
|
|
||||||
function isAccountNewerThanThresholdInMs(createdAt: string, threshold: number) {
|
export function isAccountNewerThanThresholdInMs(createdAt: string, threshold: number) {
|
||||||
let createdAtMs: number = Date.parse(createdAt);
|
let createdAtMs: number = Date.parse(createdAt);
|
||||||
if (isNaN(createdAtMs)) {
|
if (isNaN(createdAtMs)) {
|
||||||
createdAtMs = 0;
|
createdAtMs = 0;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function validateEndpointInternal(
|
|||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allowedArmEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedArmEndpoints: ReadonlyArray<string> = [
|
||||||
"https://management.azure.com",
|
"https://management.azure.com",
|
||||||
"https://management.usgovcloudapi.net",
|
"https://management.usgovcloudapi.net",
|
||||||
"https://management.chinacloudapi.cn",
|
"https://management.chinacloudapi.cn",
|
||||||
@@ -46,7 +46,7 @@ export const allowedArmEndpoints: ReadonlyArray<string> = [
|
|||||||
|
|
||||||
export const allowedAadEndpoints: ReadonlyArray<string> = ["https://login.microsoftonline.com/"];
|
export const allowedAadEndpoints: ReadonlyArray<string> = ["https://login.microsoftonline.com/"];
|
||||||
|
|
||||||
export const allowedBackendEndpoints: ReadonlyArray<string> = [
|
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
||||||
"https://main.documentdb.ext.azure.com",
|
"https://main.documentdb.ext.azure.com",
|
||||||
"https://main.documentdb.ext.azure.cn",
|
"https://main.documentdb.ext.azure.cn",
|
||||||
"https://main.documentdb.ext.azure.us",
|
"https://main.documentdb.ext.azure.us",
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||||
|
import { QueryResults } from "Contracts/ViewModels";
|
||||||
|
import { guid } from "Explorer/Tables/Utilities";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
|
|
||||||
interface QueryCopilotState {
|
interface QueryCopilotState {
|
||||||
@@ -6,20 +9,127 @@ interface QueryCopilotState {
|
|||||||
userPrompt: string;
|
userPrompt: string;
|
||||||
showFeedbackModal: boolean;
|
showFeedbackModal: boolean;
|
||||||
hideFeedbackModalForLikedQueries: boolean;
|
hideFeedbackModalForLikedQueries: boolean;
|
||||||
|
correlationId: string;
|
||||||
|
query: string;
|
||||||
|
selectedQuery: string;
|
||||||
|
isGeneratingQuery: boolean;
|
||||||
|
isExecuting: boolean;
|
||||||
|
dislikeQuery: boolean | undefined;
|
||||||
|
showCallout: boolean;
|
||||||
|
showSamplePrompts: boolean;
|
||||||
|
queryIterator: MinimalQueryIterator | undefined;
|
||||||
|
queryResults: QueryResults | undefined;
|
||||||
|
errorMessage: string;
|
||||||
|
isSamplePromptsOpen: boolean;
|
||||||
|
showDeletePopup: boolean;
|
||||||
|
showFeedbackBar: boolean;
|
||||||
|
showCopyPopup: boolean;
|
||||||
|
showErrorMessageBar: boolean;
|
||||||
|
generatedQueryComments: string;
|
||||||
|
|
||||||
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;
|
||||||
|
refreshCorrelationId: () => void;
|
||||||
|
setUserPrompt: (userPrompt: string) => void;
|
||||||
|
setQuery: (query: string) => void;
|
||||||
|
setGeneratedQuery: (generatedQuery: string) => void;
|
||||||
|
setSelectedQuery: (selectedQuery: string) => void;
|
||||||
|
setIsGeneratingQuery: (isGeneratingQuery: boolean) => void;
|
||||||
|
setIsExecuting: (isExecuting: boolean) => void;
|
||||||
|
setLikeQuery: (likeQuery: boolean) => void;
|
||||||
|
setDislikeQuery: (dislikeQuery: boolean | undefined) => void;
|
||||||
|
setShowCallout: (showCallout: boolean) => void;
|
||||||
|
setShowSamplePrompts: (showSamplePrompts: boolean) => void;
|
||||||
|
setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => void;
|
||||||
|
setQueryResults: (queryResults: QueryResults | undefined) => void;
|
||||||
|
setErrorMessage: (errorMessage: string) => void;
|
||||||
|
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => void;
|
||||||
|
setShowDeletePopup: (showDeletePopup: boolean) => void;
|
||||||
|
setShowFeedbackBar: (showFeedbackBar: boolean) => void;
|
||||||
|
setshowCopyPopup: (showCopyPopup: boolean) => void;
|
||||||
|
setShowErrorMessageBar: (showErrorMessageBar: boolean) => void;
|
||||||
|
setGeneratedQueryComments: (generatedQueryComments: string) => void;
|
||||||
|
resetQueryCopilotStates: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useQueryCopilot: UseStore<QueryCopilotState> = create((set) => ({
|
type QueryCopilotStore = UseStore<QueryCopilotState>;
|
||||||
|
|
||||||
|
export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
||||||
generatedQuery: "",
|
generatedQuery: "",
|
||||||
likeQuery: false,
|
likeQuery: false,
|
||||||
userPrompt: "",
|
userPrompt: "",
|
||||||
showFeedbackModal: false,
|
showFeedbackModal: false,
|
||||||
hideFeedbackModalForLikedQueries: false,
|
hideFeedbackModalForLikedQueries: false,
|
||||||
|
correlationId: "",
|
||||||
|
query: "",
|
||||||
|
selectedQuery: "",
|
||||||
|
isGeneratingQuery: false,
|
||||||
|
isExecuting: false,
|
||||||
|
dislikeQuery: undefined,
|
||||||
|
showCallout: false,
|
||||||
|
showSamplePrompts: false,
|
||||||
|
queryIterator: undefined,
|
||||||
|
queryResults: undefined,
|
||||||
|
errorMessage: "",
|
||||||
|
isSamplePromptsOpen: false,
|
||||||
|
showDeletePopup: false,
|
||||||
|
showFeedbackBar: false,
|
||||||
|
showCopyPopup: false,
|
||||||
|
showErrorMessageBar: false,
|
||||||
|
generatedQueryComments: "",
|
||||||
|
|
||||||
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
|
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
|
||||||
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
|
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
|
||||||
closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }),
|
closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }),
|
||||||
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
|
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
|
||||||
set({ hideFeedbackModalForLikedQueries }),
|
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 }),
|
||||||
|
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 }),
|
||||||
|
setGeneratedQueryComments: (generatedQueryComments: string) => set({ generatedQueryComments }),
|
||||||
|
|
||||||
|
resetQueryCopilotStates: () => {
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
generatedQuery: "",
|
||||||
|
likeQuery: false,
|
||||||
|
userPrompt: "",
|
||||||
|
showFeedbackModal: false,
|
||||||
|
hideFeedbackModalForLikedQueries: false,
|
||||||
|
correlationId: "",
|
||||||
|
query: "",
|
||||||
|
selectedQuery: "",
|
||||||
|
isGeneratingQuery: false,
|
||||||
|
isExecuting: false,
|
||||||
|
dislikeQuery: undefined,
|
||||||
|
showCallout: false,
|
||||||
|
showSamplePrompts: false,
|
||||||
|
queryIterator: undefined,
|
||||||
|
queryResults: undefined,
|
||||||
|
errorMessage: "",
|
||||||
|
isSamplePromptsOpen: false,
|
||||||
|
showDeletePopup: false,
|
||||||
|
showFeedbackBar: false,
|
||||||
|
showCopyPopup: false,
|
||||||
|
showErrorMessageBar: false,
|
||||||
|
generatedQueryComments: "",
|
||||||
|
}));
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { initializeIcons } from "@fluentui/react";
|
||||||
import { configure } from "enzyme";
|
import { configure } from "enzyme";
|
||||||
import Adapter from "enzyme-adapter-react-16";
|
import Adapter from "enzyme-adapter-react-16";
|
||||||
import "jest-canvas-mock";
|
import "jest-canvas-mock";
|
||||||
import { initializeIcons } from "@fluentui/react";
|
import enableHooks from "jest-react-hooks-shallow";
|
||||||
import { TextDecoder, TextEncoder } from "util";
|
import { TextDecoder, TextEncoder } from "util";
|
||||||
configure({ adapter: new Adapter() });
|
configure({ adapter: new Adapter() });
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
@@ -10,6 +11,30 @@ if (typeof window.URL.createObjectURL === "undefined") {
|
|||||||
Object.defineProperty(window.URL, "createObjectURL", { value: () => {} });
|
Object.defineProperty(window.URL, "createObjectURL", { value: () => {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableHooks(jest, { dontMockByDefault: true });
|
||||||
|
|
||||||
|
const localStorageMock = (function () {
|
||||||
|
let store: { [key: string]: string } = {};
|
||||||
|
return {
|
||||||
|
getItem: function (key: string) {
|
||||||
|
return store[key] || null;
|
||||||
|
},
|
||||||
|
setItem: function (key: string, value: string) {
|
||||||
|
store[key] = value.toString();
|
||||||
|
},
|
||||||
|
removeItem: function (key: string) {
|
||||||
|
delete store[key];
|
||||||
|
},
|
||||||
|
clear: function () {
|
||||||
|
store = {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
Object.defineProperty(window, "localStorage", {
|
||||||
|
value: localStorageMock,
|
||||||
|
});
|
||||||
|
|
||||||
// TODO Remove when jquery and documentdbclient SDK are removed
|
// TODO Remove when jquery and documentdbclient SDK are removed
|
||||||
(<any>window).$ = (<any>window).jQuery = require("jquery");
|
(<any>window).$ = (<any>window).jQuery = require("jquery");
|
||||||
(<any>global).$ = (<any>global).$.jQuery = require("jquery");
|
(<any>global).$ = (<any>global).$.jQuery = require("jquery");
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ test("Cassandra keyspace and table CRUD", async () => {
|
|||||||
await explorer.click('[data-test="New Table"]');
|
await explorer.click('[data-test="New Table"]');
|
||||||
await explorer.click('[aria-label="Keyspace id"]');
|
await explorer.click('[aria-label="Keyspace id"]');
|
||||||
await explorer.fill('[aria-label="Keyspace id"]', keyspaceId);
|
await explorer.fill('[aria-label="Keyspace id"]', keyspaceId);
|
||||||
await explorer.click('[aria-label="addCollection-tableId"]');
|
await explorer.click('[aria-label="addCollection-table Id Create table"]');
|
||||||
await explorer.fill('[aria-label="addCollection-tableId"]', tableId);
|
await explorer.fill('[aria-label="addCollection-table Id Create table"]', tableId);
|
||||||
await explorer.click("#sidePanelOkButton");
|
await explorer.click("#sidePanelOkButton");
|
||||||
await explorer.click(`.nodeItem >> text=${keyspaceId}`);
|
await explorer.click(`.nodeItem >> text=${keyspaceId}`);
|
||||||
await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`);
|
await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`);
|
||||||
|
|||||||
Reference in New Issue
Block a user