Compare commits

...

32 Commits

Author SHA1 Message Date
artrejo
6270358d9c commit current refactoring changes 2023-10-30 18:17:10 -07:00
Predrag Klepic
9f7783f3f9 [Query Copilot] Revise Copilot UI texts (#1569)
* Revise Copilot UI texts

* Test snapshots updated

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-09 21:43:55 +02:00
sindhuba
daa26d289b Fix sendMessage in DE (#1570)
* Fix issue in sendMessage in DE

* Run npm format
2023-08-09 09:28:25 -07:00
sindhuba
879cb08949 Fix issue with feature flag (#1567) 2023-08-08 08:03:42 -07:00
sindhuba
92f43c28a7 Add logic in DE to show NPS Survey (#1565)
* Send messages from DE to Portal to display NPS Survey

* Address comments
2023-08-04 13:24:30 -07:00
bogercraig
5f0c7bcea2 Users/bogercraig/endpointvalidation (#1554)
* Adding example endpoint with trailing forward slash.

* Move backend and ARM endpoint validation to configContext for initialization from config.json.

* Added debugging script and attempts to relocate endpoint validation list.

* Move default endpoint list to endpoint validation code and allow falling back to the default list during unit tests if configContext is not initialized.

* Remove leftover debugger statements.

* Remove test debug script in package.json for debugging unit tests in browser.

* Run prettier on modified files.

* Overwriting with package.json from master.

* Overwriting with version from master.

* Remove test ARM endpoint.

* Replace ternary operator with || for more concise arguments per Victor's feedback.

---------

Co-authored-by: Craig Boger <craig.boger@microsoft.com>
2023-08-03 14:47:50 -04:00
Predrag Klepic
fa55d528ad [Query Copilot] Maintain Query Copilot state when switching tabs (#1559)
* Sample implementation for saving states

* Maintaining Query Copilot state

* Fixing failed PR checks

* Additional changes based on checks

* snapshots updated

* Changes based on merging previous PR

* test mock changed

* Fixing minor bug for close button

* Destruct of queryCopilotState

* passing only function in Tabs component

* Maintaining copilot state with zustand store

* additional test changes

* test snapshot updated

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-03 09:23:31 +02:00
Predrag Klepic
c873fed7aa Disable Query Copilot Tab flickering (#1564)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-08-02 17:37:37 +02:00
v-darkora
7ec5290293 [Query Copilot] Update feature flag after sample data connection info fetch (#1560)
* Update feature flag if sample data exists

* Add additional conditional

* Revert useknockout to starting condition

* Use tracked property for rendering conditiona
2023-07-28 09:43:12 +02:00
v-darkora
4005128211 [Query Copilot] Add telemetry for query copilot (#1555) 2023-07-27 11:41:41 -07:00
v-darkora
38d13cc74e [Query Copilot] Generate query on enter (#1558) 2023-07-27 10:45:42 -07:00
Predrag Klepic
4ab93a5a32 [Query Copilot] Updated Copilot links (#1556)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-27 10:43:24 -07:00
Predrag Klepic
44e25c0769 [Query Copilot] New Query button added on Items section (#1552)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-27 10:42:17 -07:00
MokireddySampath
8ea8f0230f role has been changed to heading (#1453)
* arialabel has been added to close button of invitational youtube video

* heading role has been addedd and tag has been changed to h1

* Update QuickstartCarousel.tsx

* Update QuickstartCarousel.tsx

* Update SplashScreen.tsx
2023-07-27 07:30:27 +05:30
MokireddySampath
655b998b84 outline has been added to choose columns links (#1454)
* arialabel has been added to close button of invitational youtube video

* heading role has been addedd and tag has been changed to h1

* outline has been restored to choose columns link in entities page

* Update QuickstartCarousel.tsx

* Update SplashScreen.tsx

* Update TableEntity.tsx

* outline for edit entity has been added on focus
2023-07-27 07:29:54 +05:30
Predrag Klepic
9e2f6a9c89 [Query Copilot] QueryCopilotUtilities.ts tests (#1550)
* Improving test coverage

* Not leaving empty functions

* Additional test editing

* Correction of the unit test

* Changes made so the tests work correctly

* removing problematic tests

* QueryCopilotUtilities

* Changes based on Lint suggestion

* Changes based on lint

* Additional lint suggestion solved

* sample implementation

* removing console log

* Fixing non empty lint function error

* Changed any to void in mocked function

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-25 09:16:10 +02:00
Predrag Klepic
42e11d5160 [Query Copilot] Hide error message bar when request is successful (#1542)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-19 16:05:09 -07:00
Predrag Klepic
10037d844e [Query Copilot] Improving test coverage (#1539)
* Improving test coverage

* Not leaving empty functions

* Additional test editing

* Correction of the unit test

* Changes made so the tests work correctly

* removing problematic tests

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-19 10:13:04 +02:00
victor-meng
13434715b3 Properly check if a container is query copilot sample container (#1540) 2023-07-18 22:50:31 -07:00
v-darkora
676434cac5 [Query Copilot] Adding tests for the welcome modal (#1538) 2023-07-18 17:38:36 -07:00
Predrag Klepic
3d5580865a Query Results no longer blocked for clicking/copying content (#1537)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-17 11:15:01 -07:00
Predrag Klepic
7375cc717c [Query Copilot] Scrollable Copilot tab, improved history and minor fixes (#1536)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-17 11:10:41 -07:00
v-darkora
de3e56bb99 [Query Copilot] Add unit tests for Welcome Modal (#1535) 2023-07-14 15:18:38 -07:00
Predrag Klepic
53cd78452b [Query Copilot] Dropdown hide and buttons disabled in specific occasions (#1534)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-14 15:01:16 -07:00
v-darkora
fb6eb635c1 [Query Copilot] Handle response if it returns a 500 status (#1533) 2023-07-13 10:50:49 -07:00
v-darkora
fb6c0caca6 [Query Copilot] Update sample data resource tree item stylings (#1530)
* Update sample data resource tree item stylings

* Clean up sample data tree

---------

Co-authored-by: Victor Meng <vimeng@microsoft.com>
2023-07-13 09:17:29 +02:00
Predrag Klepic
e9f3c64239 [Query Copilot] Not disabling create database/container buttons for Sample Database (#1531)
Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
2023-07-12 16:38:42 -07:00
MokireddySampath
bb6ee5deec Required attribute added for the input in delete dialog (#1524) 2023-07-12 22:30:23 +05:30
MokireddySampath
5796b28045 alt text added for the representative images in the splashscreen home page (#1525) 2023-07-12 22:30:04 +05:30
MokireddySampath
9635d14599 removing the connect value for ariacontrols variable (#1527) 2023-07-12 22:29:43 +05:30
MokireddySampath
c58d5765c2 aria label attribute updated with label name for the input (#1532) 2023-07-12 22:29:18 +05:30
victor-meng
708f4316b4 Fix "items" button in sample data resource tree does not open documents tab (#1528) 2023-07-11 16:35:54 -07:00
83 changed files with 9445 additions and 4075 deletions

View File

@@ -230,7 +230,7 @@ input::-webkit-inner-spin-button {
.advanced-options-panel .advanced-options .select .select-options-link {
margin-left: 4px;
cursor: pointer;
outline: none;
padding: 2px;
}
.query-panel .row .column-headers .Field {

View File

@@ -28,4 +28,4 @@
.clickDisabled {
pointer-events: none;
}
}
}

8079
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -160,6 +160,7 @@
"jest": "26.6.3",
"jest-canvas-mock": "2.3.1",
"jest-playwright-preset": "1.5.1",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "0.0.7",
"less": "3.8.1",
"less-loader": "4.1.0",
@@ -232,4 +233,4 @@
"prettier": {
"printWidth": 120
}
}
}

View File

@@ -435,24 +435,89 @@ export const QueryCopilotSampleContainerId = "SampleContainer";
export const QueryCopilotSampleContainerSchema = {
product: {
sampleData: {
id: "de6fadec-0384-43c8-93ea-16c0170b5460",
name: "Premium Phone Mini (Red)",
price: 652.04,
id: "c415e70f-9bf5-4cda-aebe-a290cb8b94c2",
name: "Amazing Phone 3000 (Black)",
price: 223.33,
category: "Electronics",
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",
stock: 74,
countryOfOrigin: "Mexico",
firstAvailable: "2018-07-07 17:42:28",
priceHistory: [592.81],
"This Amazing Phone 3000 (Black) is made of black metal! It has a very well made aluminum body and it feels very comfortable. We loved the sound that comes out of it! Also, the design of the phone was a little loose at first because I was using the camera and felt uncomfortable wearing it. The phone is actually made slightly smaller than these photos! This is due to the addition of a 3.3mm filter",
stock: 84,
countryOfOrigin: "USA",
firstAvailable: "2018-09-07 19:41:44",
priceHistory: [238.68, 234.7, 221.49, 205.88, 220.15],
customerRatings: [
{ username: "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: "smithmiguel", stars: 3, date: "2022-09-08 11:43:27", verifiedUser: false },
{ username: "julie07", stars: 3, date: "2021-03-14 23:54:10", verifiedUser: true },
{ username: "kelly93", stars: 3, date: "2022-11-05 12:45:43", verifiedUser: false },
{ username: "katherinereynolds", stars: 2, date: "2022-09-14 11:49:36", verifiedUser: false },
{ username: "chandlerkenneth", stars: 1, date: "2022-01-14 12:14:43", verifiedUser: true },
{
username: "steven66",
firstName: "Carol",
gender: "female",
lastName: "Shelton",
age: "25-35",
area: "suburban",
address: "261 Collins Burgs Apt. 332\nNorth Taylor, NM 32268",
stars: 5,
date: "2021-04-22 13:42:14",
verifiedUser: true,
},
{
username: "khudson",
firstName: "Ronald",
gender: "male",
lastName: "Webb",
age: "18-24",
area: "suburban",
address: "9912 Parker Court Apt. 068\nNorth Austin, HI 76225",
stars: 5,
date: "2021-02-07 07:00:22",
verifiedUser: false,
},
{
username: "lfrancis",
firstName: "Brady",
gender: "male",
lastName: "Wright",
age: "35-45",
area: "urban",
address: "PSC 5437, Box 3159\nAPO AA 26385",
stars: 2,
date: "2022-02-23 21:40:10",
verifiedUser: false,
},
{
username: "nicolemartinez",
firstName: "Megan",
gender: "female",
lastName: "Tran",
age: "18-24",
area: "rural",
address: "7445 Salazar Brooks\nNew Sarah, PW 18097",
stars: 4,
date: "2021-09-01 22:21:40",
verifiedUser: false,
},
{
username: "uguzman",
firstName: "Deanna",
gender: "female",
lastName: "Campbell",
age: "18-24",
area: "urban",
address: "41104 Moreno Fort Suite 872\nPort Michaelbury, AK 48712",
stars: 1,
date: "2022-03-07 02:23:14",
verifiedUser: false,
},
{
username: "rebeccahunt",
firstName: "Jared",
gender: "male",
lastName: "Lopez",
age: "18-24",
area: "rural",
address: "392 Morgan Village Apt. 785\nGreenshire, CT 05921",
stars: 5,
date: "2021-04-17 04:17:49",
verifiedUser: false,
},
],
rareProperty: true,
},
@@ -494,6 +559,24 @@ export const QueryCopilotSampleContainerSchema = {
username: {
type: "string",
},
firstName: {
type: "string",
},
gender: {
type: "string",
},
lastName: {
type: "string",
},
age: {
type: "string",
},
area: {
type: "string",
},
address: {
type: "string",
},
stars: {
type: "number",
},

View File

@@ -137,7 +137,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
/>
{!isEntityValueDisable && (
<TooltipHost content="Edit property" id="editTooltip">
<div tabIndex={0}>
<div>
<Image
{...imageProps}
src={EditIcon}

View File

@@ -1,14 +1,14 @@
import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
allowedArmEndpoints,
allowedBackendEndpoints,
allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints,
allowedJunoOrigins,
allowedMongoBackendEndpoints,
allowedMsalRedirectEndpoints,
defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints,
validateEndpoint,
} from "Utils/EndpointValidation";
@@ -20,6 +20,8 @@ export enum Platform {
export interface ConfigContext {
platform: Platform;
allowedArmEndpoints: ReadonlyArray<string>;
allowedBackendEndpoints: ReadonlyArray<string>;
allowedParentFrameOrigins: ReadonlyArray<string>;
gitSha?: string;
proxyPath?: string;
@@ -49,6 +51,8 @@ export interface ConfigContext {
// Default configuration
let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal,
allowedArmEndpoints: defaultAllowedArmEndpoints,
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
allowedParentFrameOrigins: [
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
@@ -77,7 +81,7 @@ let configContext: Readonly<ConfigContext> = {
export function resetConfigContext(): void {
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;
}
@@ -87,7 +91,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
return;
}
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) {
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
delete newContext.ARM_ENDPOINT;
}
@@ -107,7 +111,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.ARCADIA_ENDPOINT;
}
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
if (
!validateEndpoint(
newContext.BACKEND_ENDPOINT,
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints
)
) {
delete newContext.BACKEND_ENDPOINT;
}
@@ -130,7 +139,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
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") {
const port: string = process.env.PORT || "1234";
updateConfigContext({

View File

@@ -1,3 +1,5 @@
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import React from "react";
import AddCollectionIcon from "../../images/AddCollection.svg";
@@ -146,7 +148,10 @@ export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] =>
if (userContext.apiType === "SQL") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot),
onClick: () => {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
},
label: "New SQL Query",
});
}

View File

@@ -25,6 +25,7 @@ export class AccordionComponent extends React.Component<AccordionComponentProps>
export interface AccordionItemComponentProps {
title: string;
isExpanded?: boolean;
containerStyles?: React.CSSProperties;
styles?: React.CSSProperties;
}
@@ -54,9 +55,9 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
}
public render(): JSX.Element {
const { styles } = this.props;
const { containerStyles, styles } = this.props;
return (
<div className="accordionItemContainer">
<div className="accordionItemContainer" style={{ ...containerStyles }}>
<div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}>
{this.renderCollapseExpandIcon()}
{this.props.title}

View File

@@ -31,6 +31,13 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
public componentDidMount(): void {
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) {

View File

@@ -5,18 +5,18 @@ import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
@@ -37,15 +37,15 @@ import {
AddMongoIndexProps,
ChangeFeedPolicyState,
GeospatialConfigType,
MongoIndexTypes,
SettingsV2TabTypes,
TtlType,
getMongoNotification,
getTabTitle,
hasDatabaseSharedThroughput,
isDirty,
MongoIndexTypes,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
SettingsV2TabTypes,
TtlType,
} from "./SettingsUtils";
interface SettingsV2TabInfo {

View File

@@ -1,5 +1,8 @@
import { Link } from "@fluentui/react/lib/Link";
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 { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
import * as ko from "knockout";
@@ -23,7 +26,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../UserContext";
import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
@@ -258,6 +261,45 @@ export default class Explorer {
// 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> {
const databaseId = userContext.parsedResourceToken?.databaseId;
const collectionId = userContext.parsedResourceToken?.collectionId;
@@ -1291,7 +1333,7 @@ export default class Explorer {
return;
}
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection);
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
useDatabases.setState({ sampleDataResourceTokenCollection });
}
}

View File

@@ -4,16 +4,21 @@
* and update any knockout observables passed from the parent.
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { AuthType } from "AuthType";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import * as React from "react";
import { userContext } from "UserContext";
import * as React from "react";
import create, { UseStore } from "zustand";
import { ConnectionStatusType, StyleConstants } from "../../../Common/Constants";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
import * as createContextCommandBarButtons from "./CommandButtonsFactories/createContextCommandBarButtons";
import * as createControlCommandBarButtons from "./CommandButtonsFactories/createControlCommandBarButtons";
import * as createPostgreButtons from "./CommandButtonsFactories/createPostgreButtons";
import * as createStaticCommandBarButtons from "./CommandButtonsFactories/createStaticCommandBarButtons";
import * as createStaticCommandBarButtonsForResourceToken from "./CommandButtonsFactories/createStaticCommandBarButtonsForResourceToken";
interface Props {
container: Explorer;
@@ -35,7 +40,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const backgroundColor = StyleConstants.BaseLight;
if (userContext.apiType === "Postgres") {
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(container);
const buttons = createPostgreButtons.createPostgreButtons(container);
return (
<div className="commandBarContainer">
<FluentCommandBar
@@ -50,11 +55,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
);
}
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
const staticButtons =
userContext.authType === AuthType.ResourceToken
? createStaticCommandBarButtonsForResourceToken.createStaticCommandBarButtonsForResourceToken(
container,
selectedNodeState
)
: createStaticCommandBarButtons.createStaticCommandBarButtons(container, selectedNodeState);
const contextButtons = (buttons || []).concat(
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState)
createContextCommandBarButtons.createContextCommandBarButtons(container, selectedNodeState)
);
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);
const controlButtons = createControlCommandBarButtons.createControlCommandBarButtons(container);
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
if (buttons && buttons.length > 0) {

View File

@@ -9,7 +9,7 @@ import NotebookManager from "../../Notebook/NotebookManager";
import { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as createStaticCommandBarButtons from "./CommandButtonsFactories/createStaticCommandBarButtons";
describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer;
@@ -32,7 +32,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Button should be visible", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@@ -48,7 +48,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@@ -64,7 +64,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@@ -100,7 +100,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
@@ -110,7 +110,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
@@ -118,7 +118,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
//TODO: modify once notebooks are available
@@ -129,7 +129,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
//TODO: modify once notebooks are available
@@ -180,7 +180,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
updateUserContext({
apiType: "SQL",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@@ -190,13 +190,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@@ -204,7 +204,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is not enabled and is available - button should be hidden", () => {
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@@ -212,7 +212,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
@@ -226,7 +226,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
@@ -241,7 +241,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
useNotebook.getState().setIsShellEnabled(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@@ -285,7 +285,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
@@ -295,13 +295,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
@@ -309,7 +309,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
@@ -317,7 +317,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
@@ -332,7 +332,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
@@ -370,7 +370,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
useNotebook.getState().setIsNotebookEnabled(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeDefined();
});
@@ -379,7 +379,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
useNotebook.getState().setIsNotebookEnabled(true);
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const manageGitHubSettingsBtn = buttons.find(
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel
);
@@ -387,7 +387,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeUndefined();
@@ -419,7 +419,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
kind: "DocumentDB",
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const buttons = createStaticCommandBarButtons.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
expect(buttons.length).toBe(2);
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
expect(buttons[0].disabled).toBe(false);

View File

@@ -1,635 +0,0 @@
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
import AddUdfIcon from "../../../../images/AddUdf.svg";
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import GitHubIcon from "../../../../images/github.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg";
import SynapseIcon from "../../../../images/synapse-link.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { Platform, configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { OpenFullScreen } from "../../OpenFullScreen";
import { AddDatabasePanel } from "../../Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
import { useDatabases } from "../../useDatabases";
import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
let counter = 0;
export function createStaticCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
if (userContext.authType === AuthType.ResourceToken) {
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
}
const newCollectionBtn = createNewCollectionGroup(container);
const buttons: CommandButtonComponentProps[] = [];
buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
buttons.push(createDivider());
buttons.push(addSynapseLink);
}
}
if (userContext.apiType !== "Tables") {
newCollectionBtn.children = [createNewCollectionGroup(container)];
const newDatabaseBtn = createNewDatabase(container);
newCollectionBtn.children.push(newDatabaseBtn);
}
if (useNotebook.getState().isNotebookEnabled) {
buttons.push(createDivider());
const notebookButtons: CommandButtonComponentProps[] = [];
const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
notebookButtons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) {
notebookButtons.push(createManageGitHubAccountButton(container));
}
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
notebookButtons.push(createOpenTerminalButton(container));
}
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
notebookButtons.push(createNotebookWorkspaceResetButton(container));
}
if (
(userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") {
notebookButtons.push(createOpenCassandraTerminalButton(container));
} else {
notebookButtons.push(createOpenMongoTerminalButton(container));
}
}
notebookButtons.forEach((btn) => {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
}
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
}
} else if (btn.commandButtonLabel.indexOf("Open Terminal") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
} else if (!useNotebook.getState().isPhoenixNotebooks) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
buttons.push(btn);
});
}
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isQuerySupported) {
buttons.push(createDivider());
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
buttons.push(newSqlQueryBtn);
}
if (isQuerySupported && selectedNodeState.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
buttons.push(openQueryBtn);
}
if (areScriptsSupported()) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState);
buttons.push(newStoredProcedureBtn);
}
}
return buttons;
}
export function createContextCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
};
buttons.push(newMongoShellBtn);
}
return buttons;
}
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [
{
iconSrc: SettingsIcon,
iconAlt: "Settings",
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane />),
commandButtonLabel: undefined,
ariaLabel: "Settings",
tooltipText: "Settings",
hasPopup: true,
disabled: false,
},
];
const showOpenFullScreen =
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
if (showOpenFullScreen) {
const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = {
id: "openFullScreenBtn",
iconSrc: OpenInTabIcon,
iconAlt: label,
onCommandClick: () => {
useSidePanel.getState().openSidePanel("Open Full Screen", <OpenFullScreen />);
},
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: !showOpenFullScreen,
className: "OpenFullScreen",
};
buttons.push(fullScreenButton);
}
if (configContext.platform !== Platform.Emulator) {
const label = "Feedback";
const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon,
iconAlt: label,
onCommandClick: () => container.provideFeedbackEmail(),
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: false,
};
buttons.push(feedbackButtonOptions);
}
return buttons;
}
export function createDivider(): CommandButtonComponentProps {
const label = `divider${counter++}`;
return {
isDivider: true,
commandButtonLabel: label,
hasPopup: false,
iconSrc: undefined,
iconAlt: undefined,
onCommandClick: undefined,
ariaLabel: label,
};
}
function areScriptsSupported(): boolean {
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
}
function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
const label = `New ${getCollectionName()}`;
return {
iconSrc: AddCollectionIcon,
iconAlt: label,
onCommandClick: () => container.onNewCollectionClicked(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
id: "createNewContainerCommandButton",
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
return undefined;
}
const capabilities = userContext?.databaseAccount?.properties?.capabilities || [];
if (capabilities.some((capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics)) {
return undefined;
}
const label = "Enable Azure Synapse Link";
return {
iconSrc: SynapseIcon,
iconAlt: label,
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() || useNotebook.getState().isSynapseLinkUpdating,
ariaLabel: label,
};
}
function createNewDatabase(container: Explorer): CommandButtonComponentProps {
const label = "New " + getDatabaseName();
return {
iconSrc: AddDatabaseIcon,
iconAlt: label,
onCommandClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
await useDatabases.getState().loadAllOffers();
}
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}
function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandButtonComponentProps {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
const label = "New SQL Query";
return {
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
} else {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
}
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
} else if (userContext.apiType === "Mongo") {
const label = "New Query";
return {
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
}
return undefined;
}
export function createScriptCommandButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean =
!selectedNodeState.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newStoredProcedureBtn);
}
if (shouldEnableScriptsCommands) {
const label = "New UDF";
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
iconSrc: AddUdfIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newUserDefinedFunctionBtn);
}
if (shouldEnableScriptsCommands) {
const label = "New Trigger";
const newTriggerBtn: CommandButtonComponentProps = {
iconSrc: AddTriggerIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newTriggerBtn);
}
return buttons;
}
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
if (!buttonProps.isDivider) {
buttonProps.disabled = true;
buttonProps.tooltipText = tooltip;
}
}
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook";
return {
id: "newNotebookBtn",
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.onNewNotebookClicked(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload to Notebook Server";
return {
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.openUploadFilePanel(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query";
return {
iconSrc: BrowseQueriesIcon,
iconAlt: label,
onCommandClick: () =>
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}
function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
const label = "Open Query From Disk";
return {
iconSrc: OpenQueryFromDiskIcon,
iconAlt: label,
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal";
return {
iconSrc: CosmosTerminalIcon,
iconAlt: label,
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label,
};
}
function createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Mongo Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? "" : tooltip,
};
}
function createOpenCassandraTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Cassandra Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? "" : tooltip,
};
}
function createOpenPsqlTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open PSQL Shell";
const disableButton =
(!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled) ||
useSelectedNode.getState().isQueryCopilotCollectionSelected();
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Postgres);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton
? ""
: "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.",
};
}
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
const label = "Reset Workspace";
return {
iconSrc: ResetWorkspaceIcon,
iconAlt: label,
onCommandClick: () => container.resetNotebookWorkspace(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
const junoClient = new JunoClient();
return {
iconSrc: GitHubIcon,
iconAlt: label,
onCommandClick: () => {
useSidePanel
.getState()
.openSidePanel(
label,
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={junoClient}
/>
);
},
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}
function createStaticCommandBarButtonsForResourceToken(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
const openQueryBtn = createOpenQueryButton(container);
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
const isResourceTokenCollectionNodeSelected: boolean =
resourceTokenCollection?.id() === selectedNodeState.selectedNode?.id();
newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
newSqlQueryBtn.onCommandClick = () => {
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
};
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
if (!openQueryBtn.disabled) {
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
}
return [newSqlQueryBtn, openQueryBtn];
}
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
const openPostgreShellBtn = createOpenPsqlTerminalButton(container);
return [openPostgreShellBtn];
}

View File

@@ -0,0 +1,8 @@
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
export function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
if (!buttonProps.isDivider) {
buttonProps.disabled = true;
buttonProps.tooltipText = tooltip;
}
}

View File

@@ -0,0 +1,5 @@
import { userContext } from "../../../../UserContext";
export function areScriptsSupported(): boolean {
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
}

View File

@@ -0,0 +1,40 @@
import * as ViewModels from "../../../../Contracts/ViewModels";
import { userContext } from "../../../../UserContext";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useNotebook } from "../../../Notebook/useNotebook";
import { SelectedNodeState } from "../../../useSelectedNode";
export function createContextCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
if (selectedNodeState.isDatabaseNodeOrNoneSelected()) {
return [];
}
if (userContext.apiType !== "Mongo") {
return [];
}
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
};
return [newMongoShellBtn];
}

View File

@@ -0,0 +1,66 @@
import * as React from "react";
import { Platform, configContext } from "../../../../ConfigContext";
import { userContext } from "../../../../UserContext";
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
import { useSidePanel } from "../../../../hooks/useSidePanel";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import SettingsIcon from "../../../../images/settings_15x15.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { OpenFullScreen } from "../../../OpenFullScreen";
import { SettingsPane } from "../../../Panes/SettingsPane/SettingsPane";
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [
{
iconSrc: SettingsIcon,
iconAlt: "Settings",
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane />),
commandButtonLabel: undefined,
ariaLabel: "Settings",
tooltipText: "Settings",
hasPopup: true,
disabled: false,
},
];
const showOpenFullScreen =
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
if (showOpenFullScreen) {
const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = {
id: "openFullScreenBtn",
iconSrc: OpenInTabIcon,
iconAlt: label,
onCommandClick: () => {
useSidePanel.getState().openSidePanel("Open Full Screen", <OpenFullScreen />);
},
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: !showOpenFullScreen,
className: "OpenFullScreen",
};
buttons.push(fullScreenButton);
}
if (configContext.platform !== Platform.Emulator) {
const label = "Feedback";
const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon,
iconAlt: label,
onCommandClick: () => container.provideFeedbackEmail(),
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
hasPopup: false,
disabled: false,
};
buttons.push(feedbackButtonOptions);
}
return buttons;
}

View File

@@ -0,0 +1,15 @@
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
let counter = 0;
export function createDivider(): CommandButtonComponentProps {
const label = `divider${counter++}`;
return {
isDivider: true,
commandButtonLabel: label,
hasPopup: false,
iconSrc: undefined,
iconAlt: undefined,
onCommandClick: undefined,
ariaLabel: label,
};
}

View File

@@ -0,0 +1,34 @@
import * as React from "react";
import { JunoClient } from "../../../../Juno/JunoClient";
import { useSidePanel } from "../../../../hooks/useSidePanel";
import GitHubIcon from "../../../../images/github.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { GitHubReposPanel } from "../../../Panes/GitHubReposPanel/GitHubReposPanel";
import { useSelectedNode } from "../../../useSelectedNode";
export function createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
const connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
const junoClient = new JunoClient();
return {
iconSrc: GitHubIcon,
iconAlt: label,
onCommandClick: () => {
useSidePanel
.getState()
.openSidePanel(
label,
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={junoClient}
/>
);
},
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}

View File

@@ -0,0 +1,17 @@
import { getCollectionName } from "../../../../Utils/APITypeUtils";
import AddCollectionIcon from "../../../../images/AddCollection.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
export function createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
const label = `New ${getCollectionName()}`;
return {
iconSrc: AddCollectionIcon,
iconAlt: label,
onCommandClick: () => container.onNewCollectionClicked(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
id: "createNewContainerCommandButton",
};
}

View File

@@ -0,0 +1,27 @@
import * as React from "react";
import { userContext } from "../../../../UserContext";
import { getDatabaseName } from "../../../../Utils/APITypeUtils";
import { useSidePanel } from "../../../../hooks/useSidePanel";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { AddDatabasePanel } from "../../../Panes/AddDatabasePanel/AddDatabasePanel";
import { useDatabases } from "../../../useDatabases";
export function createNewDatabase(container: Explorer): CommandButtonComponentProps {
const label = "New " + getDatabaseName();
return {
iconSrc: AddDatabaseIcon,
iconAlt: label,
onCommandClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
await useDatabases.getState().loadAllOffers();
}
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
};
}

View File

@@ -0,0 +1,18 @@
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useSelectedNode } from "../../../useSelectedNode";
export function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook";
return {
id: "newNotebookBtn",
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.onNewNotebookClicked(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}

View File

@@ -0,0 +1,49 @@
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { userContext } from "../../../../UserContext";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import { SelectedNodeState, useSelectedNode } from "../../../useSelectedNode";
export function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandButtonComponentProps {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
const label = "New SQL Query";
return {
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
} else {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
}
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
} else if (userContext.apiType === "Mongo") {
const label = "New Query";
return {
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
}
return undefined;
}

View File

@@ -0,0 +1,17 @@
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useSelectedNode } from "../../../useSelectedNode";
export function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
const label = "Reset Workspace";
return {
iconSrc: ResetWorkspaceIcon,
iconAlt: label,
onCommandClick: () => container.resetNotebookWorkspace(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}

View File

@@ -0,0 +1,27 @@
import * as ViewModels from "../../../../Contracts/ViewModels";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useNotebook } from "../../../Notebook/useNotebook";
export function createOpenCassandraTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Cassandra Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? "" : tooltip,
};
}

View File

@@ -0,0 +1,27 @@
import * as ViewModels from "../../../../Contracts/ViewModels";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useNotebook } from "../../../Notebook/useNotebook";
export function createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Mongo Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? "" : tooltip,
};
}

View File

@@ -0,0 +1,29 @@
import * as ViewModels from "../../../../Contracts/ViewModels";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useNotebook } from "../../../Notebook/useNotebook";
import { useSelectedNode } from "../../../useSelectedNode";
export function createOpenPsqlTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open PSQL Shell";
const disableButton =
(!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled) ||
useSelectedNode.getState().isQueryCopilotCollectionSelected();
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Postgres);
}
},
commandButtonLabel: label,
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton
? ""
: "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.",
};
}

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { useSidePanel } from "../../../../hooks/useSidePanel";
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { BrowseQueriesPane } from "../../../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { useSelectedNode } from "../../../useSelectedNode";
export function createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query";
return {
iconSrc: BrowseQueriesIcon,
iconAlt: label,
onCommandClick: () =>
useSidePanel.getState().openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={container} />),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}

View File

@@ -0,0 +1,19 @@
import * as React from "react";
import { useSidePanel } from "../../../../hooks/useSidePanel";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import { LoadQueryPane } from "../../../Panes/LoadQueryPane/LoadQueryPane";
import { useSelectedNode } from "../../../useSelectedNode";
export function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
const label = "Open Query From Disk";
return {
iconSrc: OpenQueryFromDiskIcon,
iconAlt: label,
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}

View File

@@ -0,0 +1,35 @@
import * as Constants from "../../../../Common/Constants";
import { Platform, configContext } from "../../../../ConfigContext";
import { userContext } from "../../../../UserContext";
import SynapseIcon from "../../../../images/synapse-link.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useNotebook } from "../../../Notebook/useNotebook";
import { useSelectedNode } from "../../../useSelectedNode";
export function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
return undefined;
}
const capabilities = userContext?.databaseAccount?.properties?.capabilities || [];
if (capabilities.some((capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics)) {
return undefined;
}
const label = "Enable Azure Synapse Link";
return {
iconSrc: SynapseIcon,
iconAlt: label,
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() || useNotebook.getState().isSynapseLinkUpdating,
ariaLabel: label,
};
}

View File

@@ -0,0 +1,18 @@
import * as ViewModels from "../../../../Contracts/ViewModels";
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useSelectedNode } from "../../../useSelectedNode";
export function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal";
return {
iconSrc: CosmosTerminalIcon,
iconAlt: label,
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}

View File

@@ -0,0 +1,9 @@
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { createOpenPsqlTerminalButton } from "./createOpenPsqlTerminalButton";
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
const openPostgreShellBtn = createOpenPsqlTerminalButton(container);
return [openPostgreShellBtn];
}

View File

@@ -0,0 +1,73 @@
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { SelectedNodeState, useSelectedNode } from "Explorer/useSelectedNode";
import * as ViewModels from "../../../../Contracts/ViewModels";
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
import AddUdfIcon from "../../../../images/AddUdf.svg";
import { areScriptsSupported } from "./areScriptsSupported";
export function createScriptCommandButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean =
!selectedNodeState.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newStoredProcedureBtn);
}
if (shouldEnableScriptsCommands) {
const label = "New UDF";
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
iconSrc: AddUdfIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newUserDefinedFunctionBtn);
}
if (shouldEnableScriptsCommands) {
const label = "New Trigger";
const newTriggerBtn: CommandButtonComponentProps = {
iconSrc: AddTriggerIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newTriggerBtn);
}
return buttons;
}

View File

@@ -0,0 +1,140 @@
import { createScriptCommandButtons } from "Explorer/Menus/CommandBar/CommandButtonsFactories/createScriptCommandButtons";
import * as Constants from "../../../../Common/Constants";
import { configContext } from "../../../../ConfigContext";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { userContext } from "../../../../UserContext";
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useNotebook } from "../../../Notebook/useNotebook";
import { SelectedNodeState, useSelectedNode } from "../../../useSelectedNode";
import { applyNotebooksTemporarilyDownStyle } from "./applyNotebooksTemporarilyDownStyle";
import { areScriptsSupported } from "./areScriptsSupported";
import { createDivider } from "./createDivider";
import { createManageGitHubAccountButton } from "./createManageGitHubAccountButton";
import { createNewCollectionGroup } from "./createNewCollectionGroup";
import { createNewDatabase } from "./createNewDatabase";
import { createNewNotebookButton } from "./createNewNotebookButton";
import { createNewSQLQueryButton } from "./createNewSQLQueryButton";
import { createNotebookWorkspaceResetButton } from "./createNotebookWorkspaceResetButton";
import { createOpenCassandraTerminalButton } from "./createOpenCassandraTerminalButton";
import { createOpenMongoTerminalButton } from "./createOpenMongoTerminalButton";
import { createOpenQueryButton } from "./createOpenQueryButton";
import { createOpenQueryFromDiskButton } from "./createOpenQueryFromDiskButton";
import { createOpenSynapseLinkDialogButton } from "./createOpenSynapseLinkDialogButton";
import { createOpenTerminalButton } from "./createOpenTerminalButton";
import { createuploadNotebookButton } from "./createuploadNotebookButton";
export function createStaticCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const newCollectionBtn = createNewCollectionGroup(container);
const buttons: CommandButtonComponentProps[] = [];
buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
buttons.push(createDivider());
buttons.push(addSynapseLink);
}
}
if (userContext.apiType !== "Tables") {
newCollectionBtn.children = [createNewCollectionGroup(container)];
const newDatabaseBtn = createNewDatabase(container);
newCollectionBtn.children.push(newDatabaseBtn);
}
if (useNotebook.getState().isNotebookEnabled) {
buttons.push(createDivider());
const notebookButtons: CommandButtonComponentProps[] = [];
const newNotebookButton = createNewNotebookButton(container);
newNotebookButton.children = [createNewNotebookButton(container), createuploadNotebookButton(container)];
notebookButtons.push(newNotebookButton);
if (container.notebookManager?.gitHubOAuthService) {
notebookButtons.push(createManageGitHubAccountButton(container));
}
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
notebookButtons.push(createOpenTerminalButton(container));
}
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
notebookButtons.push(createNotebookWorkspaceResetButton(container));
}
if (
(userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") {
notebookButtons.push(createOpenCassandraTerminalButton(container));
} else {
notebookButtons.push(createOpenMongoTerminalButton(container));
}
}
notebookButtons.forEach((btn) => {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
}
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
}
} else if (btn.commandButtonLabel.indexOf("Open Terminal") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
} else if (!useNotebook.getState().isPhoenixNotebooks) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
buttons.push(btn);
});
}
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isQuerySupported) {
buttons.push(createDivider());
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
buttons.push(newSqlQueryBtn);
}
if (isQuerySupported && selectedNodeState.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
buttons.push(openQueryBtn);
}
if (areScriptsSupported()) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled:
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState);
buttons.push(newStoredProcedureBtn);
}
}
return buttons;
}

View File

@@ -0,0 +1,32 @@
import * as ViewModels from "../../../../Contracts/ViewModels";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useDatabases } from "../../../useDatabases";
import { SelectedNodeState } from "../../../useSelectedNode";
import { createNewSQLQueryButton } from "./createNewSQLQueryButton";
import { createOpenQueryButton } from "./createOpenQueryButton";
import { createOpenQueryFromDiskButton } from "./createOpenQueryFromDiskButton";
export function createStaticCommandBarButtonsForResourceToken(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
const openQueryBtn = createOpenQueryButton(container);
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
const isResourceTokenCollectionNodeSelected: boolean =
resourceTokenCollection?.id() === selectedNodeState.selectedNode?.id();
newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
newSqlQueryBtn.onCommandClick = () => {
const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection;
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
};
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
if (!openQueryBtn.disabled) {
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
}
return [newSqlQueryBtn, openQueryBtn];
}

View File

@@ -0,0 +1,17 @@
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import { CommandButtonComponentProps } from "../../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../../Explorer";
import { useSelectedNode } from "../../../useSelectedNode";
export function createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload to Notebook Server";
return {
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.openUploadFilePanel(),
commandButtonLabel: label,
hasPopup: false,
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
ariaLabel: label,
};
}

View File

@@ -1425,6 +1425,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.setState({ isExecuting: false });
TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey);
useSidePanel.getState().closeSidePanel();
// open NPS Survey Dialog once the collection is created
if (userContext.features.enableNPSSurvey) {
this.props.explorer.openNPSSurveyDialog();
}
} catch (error) {
const errorMessage: string = getErrorMessage(error);
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });

View File

@@ -2,13 +2,13 @@ import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } fro
import * as Constants from "Common/Constants";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import * as SharedConstants from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
@@ -291,7 +291,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
aria-required="true"
required={true}
ariaLabel="addCollection-tableId"
ariaLabel="addCollection-table Id Create table"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"

View File

@@ -1,18 +1,18 @@
import { Text, TextField } from "@fluentui/react";
import { Areas } from "Common/Constants";
import { deleteCollection } from "Common/dataAccess/deleteCollection";
import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { deleteCollection } from "Common/dataAccess/deleteCollection";
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 { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
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 { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -126,6 +126,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
setInputCollectionName(newInput);
}}
ariaLabel={confirmContainer}
required
/>
</div>
{shouldRecordFeedback() && (

View File

@@ -44,6 +44,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
autoFocus={true}
id="confirmCollectionId"
onChange={[Function]}
required={true}
styles={
Object {
"fieldGroup": Object {
@@ -59,6 +60,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
deferredValidationTime={200}
id="confirmCollectionId"
onChange={[Function]}
required={true}
resizable={true}
styles={[Function]}
theme={
@@ -338,7 +340,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
value=""
>
<div
className="ms-TextField root-55"
className="ms-TextField is-required root-55"
>
<div
className="ms-TextField-wrapper"
@@ -356,6 +358,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
onChange={[Function]}
onFocus={[Function]}
onInput={[Function]}
required={true}
type="text"
value=""
/>

View File

@@ -140,6 +140,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
setDatabaseInput(newInput);
}}
ariaLabel={confirmDatabase}
required
/>
</div>
{isLastNonEmptyDatabase() && (

View File

@@ -112,6 +112,9 @@
margin-top: 28px;
margin-left: 4px !important;
}
.addRemoveIcon [alt="editEntity"]:focus,.addRemoveIconLabel [alt="editEntity"]:focus{
border:1px dashed #605E5C
}
.addNewParamStyle {
margin-top: 5px;
margin-left: 5px !important;

View File

@@ -371,6 +371,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
autoFocus={true}
id="confirmDatabaseId"
onChange={[Function]}
required={true}
styles={
Object {
"fieldGroup": Object {
@@ -385,6 +386,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
deferredValidationTime={200}
id="confirmDatabaseId"
onChange={[Function]}
required={true}
resizable={true}
styles={[Function]}
theme={
@@ -663,7 +665,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
validateOnLoad={true}
>
<div
className="ms-TextField root-58"
className="ms-TextField is-required root-58"
>
<div
className="ms-TextField-wrapper"
@@ -681,6 +683,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
onChange={[Function]}
onFocus={[Function]}
onInput={[Function]}
required={true}
type="text"
value=""
/>

View File

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

View File

@@ -13,7 +13,7 @@ import {
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react";
import { getUserEmail } from "../../Utils/UserUtils";
import { getUserEmail } from "../../../Utils/UserUtils";
export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
const {
@@ -28,6 +28,7 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
const [description, setDescription] = React.useState<string>("");
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
const [contact, setContact] = React.useState<string>(getUserEmail());
return (
<Modal isOpen={showFeedbackModal}>
<Stack style={{ padding: 24 }}>
@@ -77,11 +78,13 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
}}
></ChoiceGroup>
<Text style={{ fontSize: 12, marginBottom: 14 }}>
By pressing submit, your feedback will be used to improve Microsoft products and services. IT admins for your
organization will be able to view and manage your feedback data.{" "}
<Link href="" target="_blank">
Privacy statement
</Link>
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
{
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
Privacy statement
</Link>
}{" "}
for more information.
</Text>
{likeQuery && (
<Checkbox

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

View File

@@ -42,8 +42,8 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</Stack>
</Stack>
<Stack horizontalAlign="center">
<Stack.Item align="center">
<Text className="title bold">Welcome to Copilot in CosmosDB</Text>
<Stack.Item align="center" style={{ textAlign: "center" }}>
<Text className="title bold">Welcome to Query Copilot for Azure Cosmos DB (Private Preview)</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
<Stack horizontal>
@@ -52,7 +52,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</StackItem>
<StackItem align="start">
<Text className="bold">
Let copilot do the work for you
Let Query Copilot do the work for you
<br />
</Text>
</StackItem>
@@ -60,7 +60,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text>
Ask Copilot to generate a query by describing the query in your words.
<br />
<Link href="">Learn more</Link>
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
@@ -78,7 +78,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br />
<Link href="">Read preview terms</Link>
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
@@ -88,15 +88,16 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</StackItem>
<StackItem align="start">
<Text className="bold">
Copilot currently works only a sample database
Query Copilot works on a sample database.
<br />
</Text>
</StackItem>
</Stack>
<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 />
<Link href="">Learn more</Link>
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
</Text>
</Stack.Item>
</Stack>

View File

@@ -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 its 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>
`;

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Copy Popup snapshot test should render when showCopyPopup is false 1`] = `<Fragment />`;
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
<Stack
style={

View File

@@ -1,5 +1,83 @@
// 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`] = `
<Modal
isOpen={true}

View File

@@ -5,9 +5,7 @@ import { QueryCopilotTab } from "./QueryCopilotTab";
describe("Query copilot tab snapshot test", () => {
it("should render with initial input", () => {
const wrapper = shallow(
<QueryCopilotTab initialInput="Write a query to return all records in this table" explorer={new Explorer()} />
);
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-console */
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { FeedOptions } from "@azure/cosmos";
import {
Callout,
CommandBarButton,
@@ -17,16 +17,10 @@ import {
TextField,
} from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import {
QueryCopilotSampleContainerId,
QueryCopilotSampleContainerSchema,
QueryCopilotSampleDatabaseId,
} from "Common/Constants";
import { QueryCopilotSampleContainerId, QueryCopilotSampleContainerSchema } from "Common/Constants";
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { sampleDataClient } from "Common/SampleDataClient";
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { QueryResults } from "Contracts/ViewModels";
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 { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
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 { 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 { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
@@ -50,7 +46,6 @@ import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import HintIcon from "../../../images/Hint.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import RecentIcon from "../../../images/Recent.svg";
import SamplePromptsIcon from "../../../images/SamplePromptsIcon.svg";
import XErrorMessage from "../../../images/X-errorMessage.svg";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { useTabs } from "../../hooks/useTabs";
@@ -61,7 +56,6 @@ interface SuggestedPrompt {
}
interface QueryCopilotTabProps {
initialInput: string;
explorer: Explorer;
}
@@ -78,31 +72,50 @@ const promptStyles: IButtonStyles = {
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
};
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
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>("");
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({ explorer }: QueryCopilotTabProps): JSX.Element => {
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
const inputEdited = useRef(false);
const [isSamplePromptsOpen, setIsSamplePromptsOpen] = useState<boolean>(false);
const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false);
const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false);
const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false);
const [showErrorMessageBar, setShowErrorMessageBar] = useState<boolean>(false);
const {
hideFeedbackModalForLikedQueries,
userPrompt,
setUserPrompt,
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 = {
isSamplePromptsOpen: isSamplePromptsOpen,
@@ -128,12 +141,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
};
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
const cachedHistories = cachedHistoriesString?.split(",");
const cachedHistories = cachedHistoriesString?.split("|");
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
const suggestedPrompts: SuggestedPrompt[] = [
{ id: 1, text: "Give me all customers whose names start with C" },
{ id: 2, text: "Show me all customers" },
{ id: 3, text: "Show me all customers who bought a bike in 2019" },
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
];
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
@@ -155,9 +168,16 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
};
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);
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join(","));
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
};
const generateSQLQuery = async (): Promise<void> => {
@@ -170,23 +190,33 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
userPrompt: userPrompt,
};
setShowDeletePopup(false);
useQueryCopilot.getState().refreshCorrelationId();
const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", {
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
},
body: JSON.stringify(payload),
});
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
if (generateSQLQueryResponse?.sql) {
let query = `-- **Prompt:** ${userPrompt}\r\n`;
if (generateSQLQueryResponse.explanation) {
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
if (response.ok) {
if (generateSQLQueryResponse?.sql) {
let query = `-- **Prompt:** ${userPrompt}\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;
setQuery(query);
setGeneratedQuery(generateSQLQueryResponse.sql);
} else {
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
}
} catch (error) {
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> => {
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
userPrompt: userPrompt,
generatedQuery: generatedQuery,
generatedQueryComments: generatedQueryComments,
executedQuery: selectedQuery || query,
});
const queryToExecute = selectedQuery || query;
const queryIterator = querySampleDocuments(queryToExecute, {
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
@@ -233,8 +262,16 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
setQueryResults(queryResults);
setErrorMessage("");
setShowErrorMessageBar(false);
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
});
} catch (error) {
const errorMessage = getErrorMessage(error);
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId,
errorMessage: errorMessage,
});
setErrorMessage(errorMessage);
handleError(errorMessage, "executeQueryCopilotTab");
useTabs.getState().setIsQueryErrorThrown(true);
@@ -247,16 +284,17 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
const executeQueryBtn = {
const executeQueryBtn: CommandButtonComponentProps = {
iconSrc: ExecuteQueryIcon,
iconAlt: executeQueryBtnLabel,
onCommandClick: () => onExecuteQueryClick(),
commandButtonLabel: executeQueryBtnLabel,
ariaLabel: executeQueryBtnLabel,
hasPopup: false,
disabled: query?.trim() === "",
};
const saveQueryBtn = {
const saveQueryBtn: CommandButtonComponentProps = {
iconSrc: SaveQueryIcon,
iconAlt: "Save Query",
onCommandClick: () =>
@@ -264,23 +302,26 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
commandButtonLabel: "Save Query",
ariaLabel: "Save Query",
hasPopup: false,
disabled: query?.trim() === "",
};
const samplePromptsBtn = {
iconSrc: SamplePromptsIcon,
iconAlt: "Sample Prompts",
onCommandClick: () => setIsSamplePromptsOpen(true),
commandButtonLabel: "Sample Prompts",
ariaLabel: "Sample Prompts",
hasPopup: false,
};
// Sample Prompts temporary disabled due current design
// const samplePromptsBtn: CommandButtonComponentProps = {
// iconSrc: SamplePromptsIcon,
// iconAlt: "Sample Prompts",
// onCommandClick: () => setIsSamplePromptsOpen(true),
// commandButtonLabel: "Sample Prompts",
// ariaLabel: "Sample Prompts",
// hasPopup: false,
// };
return [executeQueryBtn, saveQueryBtn, samplePromptsBtn];
return [executeQueryBtn, saveQueryBtn];
};
const showTeachingBubble = (): void => {
if (!inputEdited.current) {
const shouldShowTeachingBubble = !inputEdited.current && userPrompt.trim() === "";
if (shouldShowTeachingBubble) {
setTimeout(() => {
if (!inputEdited.current) {
if (shouldShowTeachingBubble) {
toggleCopilotTeachingBubbleVisible();
inputEdited.current = true;
}
@@ -294,282 +335,305 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
setShowCallout(false);
};
const startGenerateQueryProcess = () => {
updateHistories();
generateSQLQuery();
resetButtonState();
};
React.useEffect(() => {
useCommandBar.getState().setContextButtons(getCommandbarButtons());
}, [query, selectedQuery]);
React.useEffect(() => {
if (initialInput) {
generateSQLQuery();
}
showTeachingBubble();
useTabs.getState().setIsQueryErrorThrown(false);
}, []);
return (
<Stack className="tab-pane" style={{ padding: 24, width: "100%", height: "100%" }}>
<Stack horizontal verticalAlign="center">
<Image src={CopilotIcon} />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
</Stack>
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
<TextField
id="naturalLanguageInput"
value={userPrompt}
onChange={handleUserPromptChange}
onClick={() => {
inputEdited.current = true;
setShowSamplePrompts(true);
}}
style={{ lineHeight: 30 }}
styles={{ root: { width: "95%" } }}
disabled={isGeneratingQuery}
autoComplete="off"
/>
{copilotTeachingBubbleVisible && (
<TeachingBubble
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
target="#naturalLanguageInput"
hasCloseButton={true}
closeButtonAriaLabel="Close"
onDismiss={toggleCopilotTeachingBubbleVisible}
hasSmallHeadline={true}
headline="Write a prompt"
>
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
<Link
onClick={() => {
setShowSamplePrompts(true);
toggleCopilotTeachingBubbleVisible();
}}
style={{ color: "white", fontWeight: 600 }}
<Stack className="tab-pane" style={{ padding: 24, width: "100%" }}>
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
<Stack horizontal verticalAlign="center">
<Image src={CopilotIcon} />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
</Stack>
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
<TextField
id="naturalLanguageInput"
value={userPrompt}
onChange={handleUserPromptChange}
onClick={() => {
inputEdited.current = true;
setShowSamplePrompts(true);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
startGenerateQueryProcess();
}
}}
style={{ lineHeight: 30 }}
styles={{ root: { width: "95%" } }}
disabled={isGeneratingQuery}
autoComplete="off"
/>
{copilotTeachingBubbleVisible && (
<TeachingBubble
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
target="#naturalLanguageInput"
hasCloseButton={true}
closeButtonAriaLabel="Close"
onDismiss={toggleCopilotTeachingBubbleVisible}
hasSmallHeadline={true}
headline="Write a prompt"
>
sample prompts
</Link>{" "}
or write your own query
</TeachingBubble>
)}
<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",
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
<Link
onClick={() => {
setShowSamplePrompts(true);
toggleCopilotTeachingBubbleVisible();
}}
style={{ color: "white", fontWeight: 600 }}
>
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>
))}
<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&apos;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>
sample prompts
</Link>{" "}
or write your own query
</TeachingBubble>
)}
<IconButton
id="likeBtn"
style={{ marginLeft: 20 }}
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => {
setShowCallout(!likeQuery);
setLikeQuery(!likeQuery);
if (dislikeQuery) {
setDislikeQuery(!dislikeQuery);
}
}}
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ marginLeft: 8 }}
onClick={() => startGenerateQueryProcess()}
/>
<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>
{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>
)}
{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 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)}
<Stack style={{ marginTop: 8, marginBottom: 24 }}>
<Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="http://aka.ms/cdb-copilot-preview-terms" 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>
</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}
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}
/>
)}
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
)}
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
</div>
</Stack>
);
};

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

View File

@@ -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 { 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 {
likeQuery: boolean;
@@ -21,16 +32,42 @@ export const submitFeedback = async (params: FeedbackParams): Promise<void> => {
contact: contact || "",
};
const response = await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
},
body: JSON.stringify(payload),
});
// eslint-disable-next-line no-console
console.log(response);
} catch (error) {
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();
}
};

View File

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

View File

@@ -5,133 +5,149 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
className="tab-pane"
style={
Object {
"height": "100%",
"padding": 24,
"width": "100%",
}
}
>
<Stack
horizontal={true}
verticalAlign="center"
>
<Image
src=""
/>
<Text
style={
Object {
"fontSize": 16,
"fontWeight": 600,
"marginLeft": 8,
}
}
>
Copilot
</Text>
</Stack>
<Stack
horizontal={true}
<div
style={
Object {
"marginTop": 16,
"position": "relative",
"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,
"height": "100%",
"overflowY": "auto",
}
}
>
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
<StyledLinkBase
href=""
target="_blank"
<Stack
horizontal={true}
verticalAlign="center"
>
Read preview terms
</StyledLinkBase>
</Text>
<Stack
className="tabPaneContentContainer"
>
<t
customClassName=""
onDragEnd={null}
onDragStart={null}
onSecondaryPaneSizeChange={null}
percentage={false}
primaryIndex={0}
primaryMinSize={100}
secondaryMinSize={200}
vertical={true}
<Image
src=""
/>
<Text
style={
Object {
"fontSize": 16,
"fontWeight": 600,
"marginLeft": 8,
}
}
>
Copilot
</Text>
</Stack>
<Stack
horizontal={true}
style={
Object {
"marginTop": 16,
"position": "relative",
"width": "100%",
}
}
verticalAlign="center"
>
<EditorReact
ariaLabel="Editing Query"
content=""
isReadOnly={false}
language="sql"
lineNumbers="on"
onContentChanged={[Function]}
onContentSelected={[Function]}
<StyledTextFieldBase
autoComplete="off"
disabled={false}
id="naturalLanguageInput"
onChange={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"lineHeight": 30,
}
}
styles={
Object {
"root": Object {
"width": "95%",
},
}
}
value=""
/>
<QueryResultSection
error=""
executeQueryDocumentsPage={[Function]}
isExecuting={false}
isMongoDB={false}
queryEditorContent=""
<CustomizedIconButton
disabled={true}
iconProps={
Object {
"iconName": "Send",
}
}
onClick={[Function]}
style={
Object {
"marginLeft": 8,
}
}
/>
</t>
</Stack>
<WelcomeModal
visible={true}
/>
<CopyPopup
setShowCopyPopup={[Function]}
showCopyPopup={false}
/>
</Stack>
<Stack
style={
Object {
"marginBottom": 24,
"marginTop": 8,
}
}
>
<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>
`;

View File

@@ -97,6 +97,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
() => this.setState({}),
(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 => {
if (userContext.features.enableCopilot && userContext.apiType === "SQL") {
if (
useDatabases.getState().sampleDataResourceTokenCollection &&
userContext.features.enableCopilot &&
userContext.apiType === "SQL"
) {
return (
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
<Stack horizontal tokens={{ childrenGap: 16 }}>
@@ -137,7 +147,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
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!"
}
onClick={() => useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot)}
onClick={() => {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
}}
/>
<SplashScreenButton
imgSrc={ConnectIcon}
@@ -246,8 +259,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
<form className="connectExplorerFormContainer">
<div className="splashScreenContainer">
<div className="splashScreen">
<div
<h1
className="title"
role="heading"
aria-label={
userContext.apiType === "Postgres"
? "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"}
<FeaturePanelLauncher />
</div>
</h1>
<div className="subtitle">
{userContext.apiType === "Postgres"
? "Get started with our sample datasets, documentation, and additional tools."
@@ -581,7 +595,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
>
{item.title}
</Link>
<Image src={LinkIcon} alt=" " />
<Image src={LinkIcon} alt={item.title} />
</Stack>
<Text>{item.description}</Text>
</Stack>
@@ -600,7 +614,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
<li key={`${item.title}${item.description}${index}`}>
<Stack style={{ marginBottom: 26 }}>
<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}>
{item.title}
</Link>
@@ -720,7 +734,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
>
{item.title}
</Link>
<Image src={LinkIcon} />
<Image src={LinkIcon} alt={item.title} />
</Stack>
<Text>{item.description}</Text>
</Stack>

View File

@@ -6,16 +6,16 @@ import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";

View File

@@ -1,4 +1,5 @@
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import * as ko from "knockout";
import Q from "q";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
@@ -7,7 +8,12 @@ import NewDocumentIcon from "../../../images/NewDocument.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import UploadIcon from "../../../images/Upload_16x16.svg";
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 { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
@@ -71,6 +77,7 @@ export default class DocumentsTab extends TabsBase {
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
private _resourceTokenPartitionKey: string;
private _isQueryCopilotSampleContainer: boolean;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
@@ -317,6 +324,9 @@ export default class DocumentsTab extends TabsBase {
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
this.showPartitionKey = this._shouldShowPartitionKey();
this._isQueryCopilotSampleContainer =
this.collection?.databaseId === QueryCopilotSampleDatabaseId &&
this.collection?.id() === QueryCopilotSampleContainerId;
}
private _shouldShowPartitionKey(): boolean {
@@ -678,7 +688,6 @@ export default class DocumentsTab extends TabsBase {
}
public createIterator(): QueryIterator<ItemDefinition & Resource> {
let filters = this.lastFilterContents();
const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter);
let options: any = {};
@@ -688,12 +697,16 @@ export default class DocumentsTab extends TabsBase {
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> {
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);
}
@@ -810,7 +823,7 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.newDocumentButton.enabled(),
disabled: !this.newDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
id: "mongoNewDocumentBtn",
});
}
@@ -824,7 +837,8 @@ export default class DocumentsTab extends TabsBase {
commandButtonLabel: label,
ariaLabel: label,
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,
ariaLabel: label,
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,
ariaLabel: label,
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,
ariaLabel: label,
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,
ariaLabel: label,
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,
ariaLabel: label,
hasPopup: true,
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
disabled:
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
};
}
}

View File

@@ -1,7 +1,8 @@
import { stringifyNotebook, toJS } from "@nteract/commutable";
import * as createDivider from "Explorer/Menus/CommandBar/CommandButtonsFactories/createDivider";
import { userContext } from "UserContext";
import * as ko from "knockout";
import * as Q from "q";
import { userContext } from "UserContext";
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
@@ -12,17 +13,16 @@ import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg";
import RunIcon from "../../../images/notebook/Notebook-run.svg";
import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
import * as CdbActions from "../Notebook/NotebookComponent/actions";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import * as CdbActions from "../Notebook/NotebookComponent/actions";
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
@@ -118,7 +118,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
const cellMarkdownType = "markdown";
const cellRawType = "raw";
const saveButtonChildren = [];
const saveButtonChildren: CommandButtonComponentProps[] = [];
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
saveButtonChildren.push({
iconName: copyToLabel,
@@ -272,7 +272,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
hasPopup: false,
disabled: false,
},
CommandBarComponentButtonFactory.createDivider(),
createDivider.createDivider(),
{
iconSrc: null,
iconAlt: null,

View File

@@ -7,8 +7,8 @@ import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import QueryBuilderIcon from "../../../images/Query-Builder.svg";
import QueryTextIcon from "../../../images/Query-Text.svg";
import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { userContext } from "../../UserContext";
import { useSidePanel } from "../../hooks/useSidePanel";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { AddTableEntityPanel } from "../Panes/Tables/AddTableEntityPanel";

View File

@@ -1,8 +1,8 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { Pivot, PivotItem } from "@fluentui/react";
import React from "react";
import DiscardIcon from "../../../../images/discard.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { NormalizedEventKey } from "../../../Common/Constants";
import { createStoredProcedure } from "../../../Common/dataAccess/createStoredProcedure";

View File

@@ -9,6 +9,7 @@ import { ConnectTab } from "Explorer/Tabs/ConnectTab";
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout";
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 }) {
const [hovering, setHovering] = useState(false);
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
const tabId = tab ? tab.tabId : "connect";
const tabId = tab ? tab.tabId : "";
const getReactTabTitle = (): ko.Observable<string> => {
if (tabKind === ReactTabKind.QueryCopilot) {
@@ -158,6 +159,7 @@ const CloseButton = ({
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
event.stopPropagation();
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates();
}}
tabIndex={active ? 0 : undefined}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
@@ -251,7 +253,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
case ReactTabKind.Quickstart:
return <QuickstartTab explorer={explorer} />;
case ReactTabKind.QueryCopilot:
return <QueryCopilotTab initialInput={useTabs.getState().queryCopilotTabInitialInput} explorer={explorer} />;
return <QueryCopilotTab explorer={explorer} />;
default:
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
}

View File

@@ -4,9 +4,9 @@ import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createTrigger } from "../../Common/dataAccess/createTrigger";
import { updateTrigger } from "../../Common/dataAccess/updateTrigger";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";

View File

@@ -4,9 +4,9 @@ import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";

View File

@@ -2,10 +2,10 @@ import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { useTabs } from "../../hooks/useTabs";
import Explorer from "../Explorer";
import DocumentsTab from "../Tabs/DocumentsTab";
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
@@ -28,8 +28,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
public children: ko.ObservableArray<ViewModels.TreeNode>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
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.container = container;
this.databaseId = databaseId;
@@ -42,6 +43,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
this.children = ko.observableArray<ViewModels.TreeNode>([]);
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
this.isCollectionExpanded = ko.observable<boolean>(true);
this.isSampleCollection = isSampleCollection;
}
public expandCollection(): void {
@@ -139,7 +141,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
documentsTab = new DocumentsTab({
partitionKey: this.partitionKey,
resourceTokenPartitionKey: userContext.parsedResourceToken.partitionKey,
resourceTokenPartitionKey: userContext.parsedResourceToken?.partitionKey,
documentIds: ko.observableArray<DocumentId>([]),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "Items",

View File

@@ -790,14 +790,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
{!isNotebookEnabled && isSampleDataEnabled && (
<>
<AccordionComponent>
<AccordionItemComponent
title={"MY DATA"}
isExpanded={!gitHubNotebooksContentRoot}
styles={{ maxHeight: 230 }}
>
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={"SAMPLE DATA"}>
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
</AccordionItemComponent>
</AccordionComponent>
@@ -808,14 +804,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
{isNotebookEnabled && isSampleDataEnabled && (
<>
<AccordionComponent>
<AccordionItemComponent
title={"MY DATA"}
isExpanded={!gitHubNotebooksContentRoot}
styles={{ maxHeight: 130 }}
>
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={"SAMPLE DATA"}>
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
</AccordionItemComponent>
<AccordionItemComponent title={"NOTEBOOKS"}>

View File

@@ -2,7 +2,7 @@ import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdap
import TabsBase from "Explorer/Tabs/TabsBase";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { useTabs } from "hooks/useTabs";
import React, { useEffect, useState } from "react";
import React from "react";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import * as ViewModels from "../../Contracts/ViewModels";
@@ -14,51 +14,65 @@ export const SampleDataTree = ({
}: {
sampleDataResourceTokenCollection: ViewModels.CollectionBase;
}): JSX.Element => {
const [root, setRoot] = useState<TreeNode | undefined>(undefined);
useEffect(() => {
if (sampleDataResourceTokenCollection) {
const updatedSampleTree: TreeNode = {
label: sampleDataResourceTokenCollection.databaseId,
isExpanded: false,
iconSrc: CosmosDBIcon,
className: "databaseHeader",
children: [
{
label: sampleDataResourceTokenCollection.id(),
iconSrc: CollectionIcon,
isExpanded: false,
className: "dataResourceTree",
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
onClick: () => {
// Rewritten version of expandCollapseCollection
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
useCommandBar.getState().setContextButtons([]);
useTabs().refreshActiveTab(
const buildSampleDataTree = (): TreeNode => {
const updatedSampleTree: TreeNode = {
label: sampleDataResourceTokenCollection.databaseId,
isExpanded: false,
iconSrc: CosmosDBIcon,
className: "databaseHeader",
children: [
{
label: sampleDataResourceTokenCollection.id(),
iconSrc: CollectionIcon,
isExpanded: false,
className: "collectionHeader",
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
onClick: () => {
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
useCommandBar.getState().setContextButtons([]);
useTabs
.getState()
.refreshActiveTab(
(tab: TabsBase) =>
tab.collection?.id() === sampleDataResourceTokenCollection.id() &&
tab.collection.databaseId === sampleDataResourceTokenCollection.databaseId
);
},
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(
sampleDataResourceTokenCollection.databaseId,
sampleDataResourceTokenCollection.id()
),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection),
children: [
{
label: "Items",
},
],
},
],
};
setRoot(updatedSampleTree);
}
}, [sampleDataResourceTokenCollection]);
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(sampleDataResourceTokenCollection.databaseId, sampleDataResourceTokenCollection.id()),
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." }}
/>
);
};

View File

@@ -66,11 +66,12 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
},
isQueryCopilotCollectionSelected: (): boolean => {
const selectedNode = get().selectedNode;
const selectedNode = get().selectedNode as ViewModels.CollectionBase;
if (
selectedNode &&
selectedNode.isSampleCollection &&
selectedNode.id() === QueryCopilotSampleContainerId &&
(selectedNode as ViewModels.Collection)?.databaseId === QueryCopilotSampleDatabaseId
selectedNode.databaseId === QueryCopilotSampleDatabaseId
) {
return true;
}

View File

@@ -17,7 +17,7 @@ import "../externals/jquery.typeahead.min.css";
import "../externals/jquery.typeahead.min.js";
// Image Dependencies
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 "../images/CosmosDB_rgb_ui_lighttheme.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { CommandButtonComponent } from "../../../Explorer/Controls/CommandButton/CommandButtonComponent";
import FeedbackIcon from "../../../../images/Feedback.svg";
import { CommandButtonComponent } from "../../../Explorer/Controls/CommandButton/CommandButtonComponent";
export const FeedbackCommandButton: React.FunctionComponent = () => {
return (

View File

@@ -36,6 +36,7 @@ export type Features = {
readonly enableLegacyMongoShellV2Debug: boolean;
readonly loadLegacyMongoShellFromBE: boolean;
readonly enableCopilot: boolean;
readonly enableNPSSurvey: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
@@ -104,6 +105,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
enableCopilot: "true" === get("enablecopilot"),
enableNPSSurvey: "true" === get("enablenpssurvey"),
};
}

View File

@@ -131,6 +131,9 @@ export enum Action {
LaunchUITour,
CancelUITour,
CompleteUITour,
OpenQueryCopilotFromSplashScreen,
OpenQueryCopilotFromNewQuery,
ExecuteQueryGeneratedFromQueryCopilot,
}
export const ActionModifiers = {

View File

@@ -91,7 +91,7 @@ const userContext: UserContext = {
collectionCreationDefaults: CollectionCreationDefaults,
};
function isAccountNewerThanThresholdInMs(createdAt: string, threshold: number) {
export function isAccountNewerThanThresholdInMs(createdAt: string, threshold: number) {
let createdAtMs: number = Date.parse(createdAt);
if (isNaN(createdAtMs)) {
createdAtMs = 0;

View File

@@ -38,7 +38,7 @@ function validateEndpointInternal(
return valid;
}
export const allowedArmEndpoints: ReadonlyArray<string> = [
export const defaultAllowedArmEndpoints: ReadonlyArray<string> = [
"https://management.azure.com",
"https://management.usgovcloudapi.net",
"https://management.chinacloudapi.cn",
@@ -46,7 +46,7 @@ export const allowedArmEndpoints: ReadonlyArray<string> = [
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.cn",
"https://main.documentdb.ext.azure.us",

View File

@@ -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";
interface QueryCopilotState {
@@ -6,20 +9,127 @@ interface QueryCopilotState {
userPrompt: string;
showFeedbackModal: 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;
closeFeedbackModal: () => 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: "",
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: "",
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }),
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
set({ hideFeedbackModalForLikedQueries }),
refreshCorrelationId: () => set({ correlationId: guid() }),
setUserPrompt: (userPrompt: string) => set({ userPrompt }),
setQuery: (query: string) => set({ query }),
setGeneratedQuery: (generatedQuery: string) => set({ generatedQuery }),
setSelectedQuery: (selectedQuery: string) => set({ selectedQuery }),
setIsGeneratingQuery: (isGeneratingQuery: boolean) => set({ isGeneratingQuery }),
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: "",
}));
},
}));

View File

@@ -1,7 +1,8 @@
import { initializeIcons } from "@fluentui/react";
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import "jest-canvas-mock";
import { initializeIcons } from "@fluentui/react";
import enableHooks from "jest-react-hooks-shallow";
import { TextDecoder, TextEncoder } from "util";
configure({ adapter: new Adapter() });
initializeIcons();
@@ -10,6 +11,30 @@ if (typeof window.URL.createObjectURL === "undefined") {
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
(<any>window).$ = (<any>window).jQuery = require("jquery");
(<any>global).$ = (<any>global).$.jQuery = require("jquery");

View File

@@ -16,8 +16,8 @@ test("Cassandra keyspace and table CRUD", async () => {
await explorer.click('[data-test="New Table"]');
await explorer.click('[aria-label="Keyspace id"]');
await explorer.fill('[aria-label="Keyspace id"]', keyspaceId);
await explorer.click('[aria-label="addCollection-tableId"]');
await explorer.fill('[aria-label="addCollection-tableId"]', tableId);
await explorer.click('[aria-label="addCollection-table Id Create table"]');
await explorer.fill('[aria-label="addCollection-table Id Create table"]', tableId);
await explorer.click("#sidePanelOkButton");
await explorer.click(`.nodeItem >> text=${keyspaceId}`);
await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`);