Compare commits

..

3 Commits

Author SHA1 Message Date
sunghyunkang1111
06973bd3d5 Update snapshots 2023-10-06 12:43:57 -05:00
sunghyunkang1111
44667beba9 take out toggle button when closing tab 2023-10-06 12:28:01 -05:00
sunghyunkang1111
0566f19e87 Add copilot toggle button 2023-10-06 11:36:15 -05:00
91 changed files with 2055 additions and 3138 deletions

View File

@@ -1179,16 +1179,16 @@ menuQuickStart {
} }
} }
#tbodycontent tr.gridRowSelected { .gridRowSelected {
.active(); .active();
} }
#tbodycontent tr.gridRowSelected:hover { .gridRowSelected:hover {
cursor: default; cursor: default;
.hover(); .hover();
} }
#tbodycontent tr.gridRowHighlighted { .gridRowHighlighted {
border-style: dotted; border-style: dotted;
border-width: 2px; border-width: 2px;
} }
@@ -2576,8 +2576,7 @@ a:link {
.querydropdown.placeholderVisible { .querydropdown.placeholderVisible {
font-style: italic; font-style: italic;
} }
.querydropdown.placeholderVisible::placeholder { .querydropdown.placeholderVisible::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: #767474; color: #767474;
opacity: 1; opacity: 1;
} }
@@ -2649,7 +2648,7 @@ a:link {
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText { .nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
font-weight: bolder; font-weight: bolder;
border-bottom: 2px solid rgba(0, 120, 212, 1); border-bottom: 2px solid rgba(0,120,212,1);
} }
.nav-tabs > li.active:focus > .tabNavContentContainer { .nav-tabs > li.active:focus > .tabNavContentContainer {
@@ -3097,3 +3096,4 @@ a:link {
background: white; background: white;
height: 100%; height: 100%;
} }

13
package-lock.json generated
View File

@@ -179,11 +179,10 @@
} }
}, },
"@azure/cosmos": { "@azure/cosmos": {
"version": "4.0.0", "version": "3.16.2",
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/@azure/cosmos/-/cosmos-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.16.2.tgz",
"integrity": "sha1-X9qLNctiu82lIVm5bEw5gahD1bk=", "integrity": "sha512-sceY5LWj0BHGj8PSyaVCfDRQLVZyoCfIY78kyIROJVEw0k+p9XFs8fhpykN8JklkCftL0WlaVY+X25SQwnhZsw==",
"requires": { "requires": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-auth": "^1.3.0", "@azure/core-auth": "^1.3.0",
"@azure/core-rest-pipeline": "^1.2.0", "@azure/core-rest-pipeline": "^1.2.0",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -22134,12 +22133,6 @@
"resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz", "resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz",
"integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA==" "integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA=="
}, },
"react-string-format": {
"version": "1.0.1",
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/react-string-format/-/react-string-format-1.0.1.tgz",
"integrity": "sha1-JyQaRZHqURInBBx64HC3FJBh3AA=",
"license": "MIT"
},
"react-syntax-highlighter": { "react-syntax-highlighter": {
"version": "12.2.1", "version": "12.2.1",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz",

View File

@@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.0", "@azure/cosmos": "3.16.2",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1", "@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7", "@azure/ms-rest-nodeauth": "3.0.7",
@@ -92,7 +92,6 @@
"react-notification-system": "0.2.17", "react-notification-system": "0.2.17",
"react-redux": "7.1.3", "react-redux": "7.1.3",
"react-splitter-layout": "4.0.0", "react-splitter-layout": "4.0.0",
"react-string-format": "1.0.1",
"react-youtube": "9.0.1", "react-youtube": "9.0.1",
"redux": "4.0.4", "redux": "4.0.4",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",

View File

@@ -171,7 +171,6 @@ export class Areas {
public static Tab: string = "Tab"; public static Tab: string = "Tab";
public static ShareDialog: string = "Share Access Dialog"; public static ShareDialog: string = "Share Access Dialog";
public static Notebook: string = "Notebook"; public static Notebook: string = "Notebook";
public static Copilot: string = "Copilot";
} }
export class HttpHeaders { export class HttpHeaders {

View File

@@ -125,7 +125,7 @@ describe("requestPlugin", () => {
const headers = {}; const headers = {};
const endpoint = "https://docs.azure.com"; const endpoint = "https://docs.azure.com";
const path = "/dbs/foo"; const path = "/dbs/foo";
requestPlugin({ endpoint, headers, path } as any, undefined, next as any); requestPlugin({ endpoint, headers, path } as any, next as any);
expect(next.mock.calls[0][0]).toMatchSnapshot(); expect(next.mock.calls[0][0]).toMatchSnapshot();
}); });
}); });
@@ -137,7 +137,7 @@ describe("requestPlugin", () => {
const headers = {}; const headers = {};
const endpoint = ""; const endpoint = "";
const path = "/dbs/foo"; const path = "/dbs/foo";
requestPlugin({ endpoint, headers, path } as any, undefined, next as any); requestPlugin({ endpoint, headers, path } as any, next as any);
expect(next.mock.calls[0][0]).toMatchSnapshot(); expect(next.mock.calls[0][0]).toMatchSnapshot();
}); });
}); });

View File

@@ -1,16 +1,13 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { sendCachedDataMessage } from "Common/MessageHandler"; import { configContext, Platform } from "../ConfigContext";
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../AuthType";
import { PriorityLevel } from "../Common/Constants";
import { Platform, configContext } from "../ConfigContext";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { getErrorMessage } from "./ErrorHandlingUtils"; import { getErrorMessage } from "./ErrorHandlingUtils";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { PriorityLevel } from "../Common/Constants";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { AuthType } from "../AuthType";
const _global = typeof self === "undefined" ? window : self; const _global = typeof self === "undefined" ? window : self;
@@ -29,36 +26,6 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
return decodeURIComponent(headers.authorization); return decodeURIComponent(headers.authorization);
} }
if (configContext.platform === Platform.Fabric) {
switch (requestInfo.resourceType) {
case Cosmos.ResourceType.conflicts:
case Cosmos.ResourceType.container:
case Cosmos.ResourceType.sproc:
case Cosmos.ResourceType.udf:
case Cosmos.ResourceType.trigger:
case Cosmos.ResourceType.item:
case Cosmos.ResourceType.pkranges:
// User resource tokens
headers[HttpHeaders.msDate] = new Date().toUTCString();
const resourceTokens = userContext.fabricDatabaseConnectionInfo.resourceTokens;
checkDatabaseResourceTokensValidity(userContext.fabricDatabaseConnectionInfo.resourceTokensTimestamp);
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
case Cosmos.ResourceType.none:
case Cosmos.ResourceType.database:
case Cosmos.ResourceType.offer:
case Cosmos.ResourceType.user:
case Cosmos.ResourceType.permission:
// User master tokens
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [
requestInfo,
]);
console.log("Response from Fabric: ", authorizationToken);
headers[HttpHeaders.msDate] = authorizationToken.XDate;
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
}
}
if (userContext.masterKey) { if (userContext.masterKey) {
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK. // TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey); await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
@@ -74,7 +41,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
return decodeURIComponent(result.PrimaryReadWriteToken); return decodeURIComponent(result.PrimaryReadWriteToken);
}; };
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, diagnosticNode, next) => { export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href; requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href;
requestContext.headers["x-ms-proxy-target"] = endpoint(); requestContext.headers["x-ms-proxy-target"] = endpoint();
return next(requestContext); return next(requestContext);
@@ -89,11 +56,7 @@ export const endpoint = () => {
return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint; return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
}; };
export async function getTokenFromAuthService( export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> {
verb: string,
resourceType: string,
resourceId?: string,
): Promise<AuthorizationToken> {
try { try {
const host = configContext.BACKEND_ENDPOINT; const host = configContext.BACKEND_ENDPOINT;
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", { const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
@@ -148,6 +111,9 @@ export function client(): Cosmos.CosmosClient {
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
key: userContext.masterKey, key: userContext.masterKey,
tokenProvider, tokenProvider,
connectionPolicy: {
enableEndpointDiscovery: false,
},
userAgentSuffix: "Azure Portal", userAgentSuffix: "Azure Portal",
defaultHeaders: _defaultHeaders, defaultHeaders: _defaultHeaders,
}; };

View File

@@ -22,7 +22,7 @@ export function handleCachedDataMessage(message: any): void {
if (messageContent.error != null) { if (messageContent.error != null) {
cachedDataPromise.deferred.reject(messageContent.error); cachedDataPromise.deferred.reject(messageContent.error);
} else { } else {
cachedDataPromise.deferred.resolve(messageContent.data); cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
} }
runGarbageCollector(); runGarbageCollector();
} }

View File

@@ -1,57 +1,18 @@
import { ContainerResponse } from "@azure/cosmos";
import { Queries } from "Common/Constants"; import { Queries } from "Common/Constants";
import { Platform, configContext } from "ConfigContext";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; import { listMongoDBCollections } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { listSqlContainers } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { listSqlContainers } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { listTables } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import { listTables } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> { export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
if (
configContext.platform === Platform.Fabric &&
userContext.fabricDatabaseConnectionInfo &&
userContext.fabricDatabaseConnectionInfo.databaseId === databaseId
) {
const collections: DataModels.Collection[] = [];
const promises: Promise<ContainerResponse>[] = [];
for (const collectionResourceId in userContext.fabricDatabaseConnectionInfo.resourceTokens) {
// Dictionary key looks like this: dbs/SampleDB/colls/Container
const resourceIdObj = collectionResourceId.split("/");
const tokenDatabaseId = resourceIdObj[1];
const tokenCollectionId = resourceIdObj[3];
if (tokenDatabaseId === databaseId) {
promises.push(client().database(databaseId).container(tokenCollectionId).read());
}
}
try {
const responses = await Promise.all(promises);
responses.forEach((response) => {
collections.push(response.resource as DataModels.Collection);
});
// Sort collections by id before returning
collections.sort((a, b) => a.id.localeCompare(b.id));
return collections;
} catch (error) {
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
throw error;
} finally {
clearMessage();
}
}
try { try {
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&

View File

@@ -1,22 +1,15 @@
import { Platform, configContext } from "ConfigContext";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels"; import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { readOfferWithSDK } from "./readOfferWithSDK"; import { readOfferWithSDK } from "./readOfferWithSDK";
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => { export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
if (configContext.platform === Platform.Fabric) {
// TODO This works, but is very slow, because it requests the token, so we skip for now
console.error("Skiping readDatabaseOffer for Fabric");
return undefined;
}
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`); const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
try { try {

View File

@@ -1,53 +1,17 @@
import { Platform, configContext } from "ConfigContext";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { listSqlDatabases } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { listSqlDatabases } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
export async function readDatabases(): Promise<DataModels.Database[]> { export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[]; let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`); const clearMessage = logConsoleProgress(`Querying databases`);
if (configContext.platform === Platform.Fabric && userContext.fabricDatabaseConnectionInfo?.resourceTokens) {
const tokensData = userContext.fabricDatabaseConnectionInfo;
const databaseIdsSet = new Set<string>(); // databaseId
for (const collectionResourceId in tokensData.resourceTokens) {
// Dictionary key looks like this: dbs/SampleDB/colls/Container
const resourceIdObj = collectionResourceId.split("/");
if (resourceIdObj.length !== 4) {
handleError(`Resource key not recognized: ${resourceIdObj}`, "ReadDatabases", `Error while querying databases`);
clearMessage();
return [];
}
const databaseId = resourceIdObj[1];
databaseIdsSet.add(databaseId);
}
const databases: DataModels.Database[] = Array.from(databaseIdsSet.values())
.sort((a, b) => a.localeCompare(b))
.map((databaseId) => ({
_rid: "",
_self: "",
_etag: "",
_ts: 0,
id: databaseId,
collections: [],
}));
clearMessage();
return databases;
}
try { try {
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&

View File

@@ -1,66 +0,0 @@
export function getAuthorizationTokenUsingResourceTokens(
resourceTokens: { [resourceId: string]: string },
path: string,
resourceId: string,
): string {
// console.log(`getting token for path: "${path}" and resourceId: "${resourceId}"`);
if (resourceTokens && Object.keys(resourceTokens).length > 0) {
// For database account access(through getDatabaseAccount API), path and resourceId are "",
// so in this case we return the first token to be used for creating the auth header as the
// service will accept any token in this case
if (!path && !resourceId) {
return resourceTokens[Object.keys(resourceTokens)[0]];
}
// If we have exact resource token for the path use it
if (resourceId && resourceTokens[resourceId]) {
return resourceTokens[resourceId];
}
// minimum valid path /dbs
if (!path || path.length < 4) {
console.error(
`Unable to get authotization token for Path:"${path}" and resourcerId:"${resourceId}". Invalid path.`,
);
return null;
}
path = trimSlashFromLeftAndRight(path);
const pathSegments = (path && path.split("/")) || [];
// Item path
if (pathSegments.length === 6) {
// Look for a container token matching the item path
const containerPath = pathSegments.slice(0, 4).map(decodeURIComponent).join("/");
if (resourceTokens[containerPath]) {
return resourceTokens[containerPath];
}
}
// This is legacy behavior that lets someone use a resource token pointing ONLY at an ID
// It was used when _rid was exposed by the SDK, but now that we are using user provided ids it is not needed
// However removing it now would be a breaking change
// if it's an incomplete path like /dbs/db1/colls/, start from the parent resource
let index = pathSegments.length % 2 === 0 ? pathSegments.length - 1 : pathSegments.length - 2;
for (; index > 0; index -= 2) {
const id = decodeURI(pathSegments[index]);
if (resourceTokens[id]) {
return resourceTokens[id];
}
}
}
console.error(`Unable to get authotization token for Path:"${path}" and resourcerId:"${resourceId}"`);
return null;
}
const trimLeftSlashes = new RegExp("^[/]+");
const trimRightSlashes = new RegExp("[/]+$");
function trimSlashFromLeftAndRight(inputString: string): string {
if (typeof inputString !== "string") {
throw new Error("invalid input: input is not string");
}
return inputString.replace(trimLeftSlashes, "").replace(trimRightSlashes, "");
}

View File

@@ -1,4 +1,3 @@
import { JunoEndpoints } from "Common/Constants";
import { import {
allowedAadEndpoints, allowedAadEndpoints,
allowedArcadiaEndpoints, allowedArcadiaEndpoints,
@@ -65,7 +64,6 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`, `^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
`^https:\\/\\/.*\\.powerbi\\.com$`, `^https:\\/\\/.*\\.powerbi\\.com$`,
`^https:\\/\\/.*\\.analysis-df\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
], // Webpack injects this at build time ], // Webpack injects this at build time
gitSha: process.env.GIT_SHA, gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/", hostedExplorerURL: "https://cosmos.azure.com/",
@@ -79,7 +77,7 @@ let configContext: Readonly<ConfigContext> = {
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net", ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306 GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
JUNO_ENDPOINT: JunoEndpoints.Prod, JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
isTerminalEnabled: false, isTerminalEnabled: false,
isPhoenixEnabled: false, isPhoenixEnabled: false,

View File

@@ -1,13 +0,0 @@
export interface QueryRequestOptions {
$skipToken?: string;
$top?: number;
subscriptions: string[];
}
export interface QueryResponse {
$skipToken: string;
count: number;
data: any;
resultTruncated: boolean;
totalRecords: number;
}

View File

@@ -88,13 +88,13 @@ export interface GenerateTokenResponse {
} }
export interface Subscription { export interface Subscription {
uniqueDisplayName?: string; uniqueDisplayName: string;
displayName: string; displayName: string;
subscriptionId: string; subscriptionId: string;
tenantId?: string; tenantId: string;
state: string; state: string;
subscriptionPolicies?: SubscriptionPolicies; subscriptionPolicies: SubscriptionPolicies;
authorizationSource?: string; authorizationSource: string;
} }
export interface SubscriptionPolicies { export interface SubscriptionPolicies {
@@ -457,11 +457,8 @@ export interface ContainerInfo {
} }
export interface IProvisionData { export interface IProvisionData {
cosmosEndpoint?: string; cosmosEndpoint: string;
poolId: string; poolId: string;
databaseId?: string;
containerId?: string;
mode?: string;
} }
export interface IContainerData { export interface IContainerData {
@@ -604,14 +601,3 @@ export enum PhoenixErrorType {
PhoenixFlightFallback = "PhoenixFlightFallback", PhoenixFlightFallback = "PhoenixFlightFallback",
UserMissingPermissionsError = "UserMissingPermissionsError", UserMissingPermissionsError = "UserMissingPermissionsError",
} }
export interface CopilotEnabledConfiguration {
isEnabled: boolean;
}
export interface FeatureRegistration {
name: string;
properties: {
state: string;
};
}

View File

@@ -1,6 +1,46 @@
import { MessageTypes } from "Contracts/MessageTypes";
import * as ActionContracts from "./ActionContracts"; import * as ActionContracts from "./ActionContracts";
import * as Diagnostics from "./Diagnostics"; import * as Diagnostics from "./Diagnostics";
import * as Versions from "./Versions"; import * as Versions from "./Versions";
export { ActionContracts, Diagnostics, MessageTypes, Versions }; /**
* Messaging types used with Data Explorer <-> Portal communication
* and Hosted <-> Explorer communication
*/
export enum MessageTypes {
TelemetryInfo,
LogInfo,
RefreshResources,
AllDatabases,
CollectionsForDatabase,
RefreshOffers,
AllOffers,
UpdateLocationHash,
SingleOffer,
RefreshOffer,
UpdateAccountName,
ForbiddenError,
AadSignIn,
GetAccessAadRequest,
GetAccessAadResponse,
UpdateAccountSwitch,
UpdateDirectoryControl,
SwitchAccount,
SendNotification,
ClearNotification,
ExplorerClickEvent,
LoadingStatus,
GetArcadiaToken,
CreateWorkspace,
CreateSparkPool,
RefreshDatabaseAccount,
CloseTab,
OpenQuickstartBlade,
OpenPostgreSQLPasswordReset,
OpenPostgresNetworkingBlade,
OpenCosmosDBNetworkingBlade,
DisplayNPSSurvey,
OpenVCoreMongoNetworkingBlade,
OpenVCoreMongoConnectionStringsBlade,
}
export { ActionContracts, Diagnostics, Versions };

View File

@@ -1,5 +1,3 @@
import { AuthorizationToken, MessageTypes } from "./MessageTypes";
export type FabricMessage = export type FabricMessage =
| { | {
type: "newContainer"; type: "newContainer";
@@ -7,71 +5,21 @@ export type FabricMessage =
} }
| { | {
type: "initialize"; type: "initialize";
message: { connectionString: string | undefined;
endpoint: string | undefined;
databaseId: string | undefined;
resourceTokens: unknown | undefined;
resourceTokensTimestamp: number | undefined;
error: string | undefined;
};
} }
| { | {
type: "authorizationToken"; type: "openTab";
message: { databaseName: string;
id: string; collectionName: string | undefined;
error: string | undefined;
data: AuthorizationToken | undefined;
};
}
| {
type: "allResourceTokens";
message: {
endpoint: string | undefined;
databaseId: string | undefined;
resourceTokens: unknown | undefined;
resourceTokensTimestamp: number | undefined;
};
}; };
export type DataExploreMessage = export type DataExploreMessage =
| "ready" | "ready"
| { | {
type: MessageTypes.TelemetryInfo; type: number;
data: { data: {
action: "LoadDatabases"; action: "LoadDatabases";
actionModifier: "success" | "start"; actionModifier: "success" | "start";
defaultExperience: "SQL"; defaultExperience: "SQL";
}; };
}
| {
type: MessageTypes.GetAuthorizationToken;
id: string;
params: GetCosmosTokenMessageOptions[];
}
| {
type: MessageTypes.GetAllResourceTokens;
}; };
export type GetCosmosTokenMessageOptions = {
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
resourceId: string;
};
export type CosmosDBTokenResponse = {
token: string;
date: string;
};
export type CosmosDBConnectionInfoResponse = {
endpoint: string;
databaseId: string;
resourceTokens: unknown;
};
export interface FabricDatabaseConnectionInfo {
endpoint: string;
databaseId: string;
resourceTokens: { [resourceId: string]: string };
resourceTokensTimestamp: number;
}

View File

@@ -1,49 +0,0 @@
/**
* Messaging types used with Data Explorer <-> Portal communication,
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
*/
export enum MessageTypes {
TelemetryInfo,
LogInfo,
RefreshResources,
AllDatabases,
CollectionsForDatabase,
RefreshOffers,
AllOffers,
UpdateLocationHash,
SingleOffer,
RefreshOffer,
UpdateAccountName,
ForbiddenError,
AadSignIn,
GetAccessAadRequest,
GetAccessAadResponse,
UpdateAccountSwitch,
UpdateDirectoryControl,
SwitchAccount,
SendNotification,
ClearNotification,
ExplorerClickEvent,
LoadingStatus,
GetArcadiaToken,
CreateWorkspace,
CreateSparkPool,
RefreshDatabaseAccount,
CloseTab,
OpenQuickstartBlade,
OpenPostgreSQLPasswordReset,
OpenPostgresNetworkingBlade,
OpenCosmosDBNetworkingBlade,
DisplayNPSSurvey,
OpenVCoreMongoNetworkingBlade,
OpenVCoreMongoConnectionStringsBlade,
// Data Explorer -> Fabric communication
GetAuthorizationToken,
GetAllResourceTokens,
}
export interface AuthorizationToken {
XDate: string;
PrimaryReadWriteToken: string;
}

View File

@@ -1,4 +0,0 @@
declare module "*.less" {
const value: string;
export default value;
}

View File

@@ -129,22 +129,20 @@ export const createCollectionContextMenuButton = (
}); });
} }
if (configContext.platform !== Platform.Fabric) { items.push({
items.push({ iconSrc: DeleteCollectionIcon,
iconSrc: DeleteCollectionIcon, onClick: () => {
onClick: () => { useSelectedNode.getState().setSelectedNode(selectedCollection);
useSelectedNode.getState().setSelectedNode(selectedCollection); useSidePanel
useSidePanel .getState()
.getState() .openSidePanel(
.openSidePanel( "Delete " + getCollectionName(),
"Delete " + getCollectionName(), <DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />, );
); },
}, label: `Delete ${getCollectionName()}`,
label: `Delete ${getCollectionName()}`, styleClass: "deleteCollectionMenuItem",
styleClass: "deleteCollectionMenuItem", });
});
}
return items; return items;
}; };

View File

@@ -48,7 +48,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
public componentDidUpdate(previous: EditorReactProps) { public componentDidUpdate(previous: EditorReactProps) {
if (this.props.content !== previous.content) { if (this.props.content !== previous.content) {
this.editor?.setValue(this.props.content); this.editor.setValue(this.props.content);
} }
} }
@@ -111,7 +111,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
this.rootNode.innerHTML = ""; this.rootNode.innerHTML = "";
const monaco = await loadMonaco(); const monaco = await loadMonaco();
createCallback(monaco?.editor?.create(this.rootNode, options)); createCallback(monaco.editor.create(this.rootNode, options));
if (this.rootNode.innerHTML) { if (this.rootNode.innerHTML) {
this.setState({ this.setState({

View File

@@ -123,6 +123,19 @@ describe("ContainerSampleGenerator", () => {
await generator.createSampleContainerAsync(); await generator.createSampleContainerAsync();
}); });
it("should not create any sample for Mongo API account", async () => {
const experience = "Sample generation not supported for this API Mongo";
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableMongo" }],
},
} as DatabaseAccount,
});
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
});
it("should not create any sample for Table API account", async () => { it("should not create any sample for Table API account", async () => {
const experience = "Sample generation not supported for this API Tables"; const experience = "Sample generation not supported for this API Tables";
updateUserContext({ updateUserContext({

View File

@@ -3,9 +3,7 @@ import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler"; import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { IGalleryItem } from "Juno/JunoClient"; import { IGalleryItem } from "Juno/JunoClient";
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout"; import * as ko from "knockout";
@@ -93,7 +91,7 @@ export default class Explorer {
}; };
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
public phoenixClient: PhoenixClient; private phoenixClient: PhoenixClient;
constructor() { constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
@@ -275,7 +273,6 @@ export default class Explorer {
const NINETY_DAYS_IN_MS = 7776000000; const NINETY_DAYS_IN_MS = 7776000000;
const ONE_DAY_IN_MS = 86400000; const ONE_DAY_IN_MS = 86400000;
const THREE_DAYS_IN_MS = 259200000;
const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs( const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs(
userContext.databaseAccount?.systemData?.createdAt || "", userContext.databaseAccount?.systemData?.createdAt || "",
NINETY_DAYS_IN_MS, NINETY_DAYS_IN_MS,
@@ -295,32 +292,32 @@ export default class Explorer {
} }
} }
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer. // Try Cosmos DB subscription - survey shown to random 25% of users at day 1 in Data Explorer.
if (userContext.isTryCosmosDBSubscription) { if (userContext.isTryCosmosDBSubscription) {
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) { if (
this.sendNPSMessage(); isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS) &&
this.getRandomInt(100) < 25
) {
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
localStorage.setItem("lastSubmitted", Date.now().toString());
} }
} else { } else {
// An existing account is older than 3 days but less than 90 days old. For existing account show to 100% of users in Data Explorer. // An existing account is lesser than 90 days old. For existing account show to random 10 % of users in Data Explorer.
if ( if (isAccountNewerThanNinetyDays) {
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS) && if (this.getRandomInt(100) < 10) {
isAccountNewerThanNinetyDays sendMessage({ type: MessageTypes.DisplayNPSSurvey });
) { localStorage.setItem("lastSubmitted", Date.now().toString());
this.sendNPSMessage(); }
} else { } else {
// An existing account is greater than 90 days. For existing account show to random 33% of users in Data Explorer. // An existing account is greater than 90 days. For existing account show to random 25 % of users in Data Explorer.
if (this.getRandomInt(100) < 33) { if (this.getRandomInt(100) < 25) {
this.sendNPSMessage(); sendMessage({ type: MessageTypes.DisplayNPSSurvey });
localStorage.setItem("lastSubmitted", Date.now().toString());
} }
} }
} }
} }
private sendNPSMessage() {
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
localStorage.setItem("lastSubmitted", Date.now().toString());
}
public async refreshDatabaseForResourceToken(): Promise<void> { public async refreshDatabaseForResourceToken(): Promise<void> {
const databaseId = userContext.parsedResourceToken?.databaseId; const databaseId = userContext.parsedResourceToken?.databaseId;
const collectionId = userContext.parsedResourceToken?.collectionId; const collectionId = userContext.parsedResourceToken?.collectionId;
@@ -382,13 +379,6 @@ export default class Explorer {
}; };
public onRefreshResourcesClick = (): void => { public onRefreshResourcesClick = (): void => {
if (configContext.platform === Platform.Fabric) {
// Requesting the tokens will trigger a refresh of the databases
// TODO: Once the id is returned from Fabric, we can await this call and then refresh the databases here
requestDatabaseResourceTokens();
return;
}
userContext.authType === AuthType.ResourceToken userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(); : this.refreshAllDatabases();
@@ -413,7 +403,7 @@ export default class Explorer {
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
} }
public async allocateContainer(poolId: PoolIdType, mode?: string): Promise<void> { public async allocateContainer(poolId: PoolIdType): Promise<void> {
const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false; const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false;
const notebookServerInfo = shouldUseNotebookStates const notebookServerInfo = shouldUseNotebookStates
? useNotebook.getState().notebookServerInfo ? useNotebook.getState().notebookServerInfo
@@ -427,6 +417,10 @@ export default class Explorer {
(notebookServerInfo === undefined || (notebookServerInfo === undefined ||
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined)) (notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
) { ) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
poolId: shouldUseNotebookStates ? undefined : poolId,
};
const connectionStatus: ContainerConnectionInfo = { const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting, status: ConnectionStatusType.Connecting,
}; };
@@ -434,26 +428,14 @@ export default class Explorer {
shouldUseNotebookStates && useNotebook.getState().setConnectionInfo(connectionStatus); shouldUseNotebookStates && useNotebook.getState().setConnectionInfo(connectionStatus);
let connectionInfo; let connectionInfo;
let provisionData: IProvisionData;
try { try {
TelemetryProcessor.traceStart(Action.PhoenixConnection, { TelemetryProcessor.traceStart(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook, dataExplorerArea: Areas.Notebook,
}); });
if (shouldUseNotebookStates) { shouldUseNotebookStates
useNotebook.getState().setIsAllocating(true); ? useNotebook.getState().setIsAllocating(true)
provisionData = { : useQueryCopilot.getState().setIsAllocatingContainer(true);
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
poolId: undefined,
};
} else {
useQueryCopilot.getState().setIsAllocatingContainer(true);
provisionData = {
poolId: poolId,
databaseId: useTabs.getState().activeTab.collection.databaseId,
containerId: useTabs.getState().activeTab.collection.id(),
mode: mode,
};
}
connectionInfo = await this.phoenixClient.allocateContainer(provisionData); connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
if (!connectionInfo?.data?.phoenixServiceUrl) { if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`PhoenixServiceUrl is invalid!`); throw new Error(`PhoenixServiceUrl is invalid!`);
@@ -469,21 +451,19 @@ export default class Explorer {
error: getErrorMessage(error), error: getErrorMessage(error),
errorStack: getErrorStack(error), errorStack: getErrorStack(error),
}); });
if (shouldUseNotebookStates) { connectionStatus.status = ConnectionStatusType.Failed;
connectionStatus.status = ConnectionStatusType.Failed; shouldUseNotebookStates
shouldUseNotebookStates ? useNotebook.getState().resetContainerConnection(connectionStatus)
? useNotebook.getState().resetContainerConnection(connectionStatus) : useQueryCopilot.getState().resetContainerConnection();
: useQueryCopilot.getState().resetContainerConnection(); if (error?.status === HttpStatusCodes.Forbidden && error.message) {
if (error?.status === HttpStatusCodes.Forbidden && error.message) { useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`); } else {
} else { useDialog
useDialog .getState()
.getState() .showOkModalDialog(
.showOkModalDialog( "Connection Failed",
"Connection Failed", "We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.", );
);
}
} }
throw error; throw error;
} finally { } finally {
@@ -497,11 +477,11 @@ export default class Explorer {
} }
} }
public async setNotebookInfo( private async setNotebookInfo(
shouldUseNotebookStates: boolean, shouldUseNotebookStates: boolean,
connectionInfo: IResponse<IPhoenixServiceInfo>, connectionInfo: IResponse<IPhoenixServiceInfo>,
connectionStatus: DataModels.ContainerConnectionInfo, connectionStatus: DataModels.ContainerConnectionInfo,
): Promise<void> { ) {
const containerData = { const containerData = {
forwardingId: connectionInfo.data.forwardingId, forwardingId: connectionInfo.data.forwardingId,
dbAccountName: userContext.databaseAccount.name, dbAccountName: userContext.databaseAccount.name,
@@ -522,7 +502,6 @@ export default class Explorer {
shouldUseNotebookStates shouldUseNotebookStates
? useNotebook.getState().setNotebookServerInfo(noteBookServerInfo) ? useNotebook.getState().setNotebookServerInfo(noteBookServerInfo)
: useQueryCopilot.getState().setNotebookServerInfo(noteBookServerInfo); : useQueryCopilot.getState().setNotebookServerInfo(noteBookServerInfo);
shouldUseNotebookStates && shouldUseNotebookStates &&
this.notebookManager?.notebookClient this.notebookManager?.notebookClient
.getMemoryUsage() .getMemoryUsage()
@@ -1385,21 +1364,6 @@ export default class Explorer {
await this.refreshSampleData(); await this.refreshSampleData();
} }
public async configureCopilot(): Promise<void> {
if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
return;
}
const copilotEnabledPromise = getCopilotEnabled();
const copilotUserDBEnabledPromise = isCopilotFeatureRegistered(userContext.subscriptionId);
const [copilotEnabled, copilotUserDBEnabled] = await Promise.all([
copilotEnabledPromise,
copilotUserDBEnabledPromise,
]);
useQueryCopilot.getState().setCopilotEnabled(copilotEnabled);
useQueryCopilot.getState().setCopilotUserDBEnabled(copilotUserDBEnabled);
useQueryCopilot.getState().setShowWelcomeModal(window.localStorage.getItem("hideWelcomeModal") !== "true");
}
public async refreshSampleData(): Promise<void> { public async refreshSampleData(): Promise<void> {
if (!userContext.sampleDataConnectionInfo) { if (!userContext.sampleDataConnectionInfo) {
return; return;

View File

@@ -345,7 +345,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Open Postgres and vCore Mongo buttons", () => { describe("Open Postgres and vCore Mongo buttons", () => {
const openPostgresShellButtonLabel = "Open PSQL shell"; const openPostgresShellButtonLabel = "Open PSQL shell";
const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell"; const openVCoreMongoShellButtonLabel = "Open MongoDB (vcore) shell";
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;

View File

@@ -1,3 +1,6 @@
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg"; import AddCollectionIcon from "../../../../images/AddCollection.svg";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg"; import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
@@ -47,36 +50,31 @@ export function createStaticCommandBarButtons(
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState); return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
} }
const newCollectionBtn = createNewCollectionGroup(container);
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
// Avoid starting with a divider buttons.push(newCollectionBtn);
const addDivider = () => { if (
if (buttons.length > 0) { configContext.platform !== Platform.Fabric &&
userContext.apiType !== "Tables" &&
userContext.apiType !== "Cassandra"
) {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
buttons.push(createDivider()); buttons.push(createDivider());
} buttons.push(addSynapseLink);
};
if (configContext.platform !== Platform.Fabric) {
const newCollectionBtn = createNewCollectionGroup(container);
buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
addDivider();
buttons.push(addSynapseLink);
}
}
if (userContext.apiType !== "Tables") {
newCollectionBtn.children = [createNewCollectionGroup(container)];
const newDatabaseBtn = createNewDatabase(container);
newCollectionBtn.children.push(newDatabaseBtn);
} }
} }
if (userContext.apiType !== "Tables") {
newCollectionBtn.children = [createNewCollectionGroup(container)];
const newDatabaseBtn = createNewDatabase(container);
newCollectionBtn.children.push(newDatabaseBtn);
}
if (useNotebook.getState().isNotebookEnabled) { if (useNotebook.getState().isNotebookEnabled) {
addDivider(); buttons.push(createDivider());
const notebookButtons: CommandButtonComponentProps[] = []; const notebookButtons: CommandButtonComponentProps[] = [];
const newNotebookButton = createNewNotebookButton(container); const newNotebookButton = createNewNotebookButton(container);
@@ -130,7 +128,7 @@ export function createStaticCommandBarButtons(
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isQuerySupported) { if (isQuerySupported) {
addDivider(); buttons.push(createDivider());
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState); const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
buttons.push(newSqlQueryBtn); buttons.push(newSqlQueryBtn);
} }
@@ -334,8 +332,13 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection); useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
} else {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
}
}, },
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
@@ -515,7 +518,7 @@ function createOpenTerminalButtonByKind(
case ViewModels.TerminalKind.Postgres: case ViewModels.TerminalKind.Postgres:
return "PSQL"; return "PSQL";
case ViewModels.TerminalKind.VCoreMongo: case ViewModels.TerminalKind.VCoreMongo:
return "MongoDB (vCore)"; return "MongoDB (vcore)";
default: default:
return ""; return "";
} }

View File

@@ -6,7 +6,6 @@ import {
IDropdownOption, IDropdownOption,
IDropdownStyles, IDropdownStyles,
} from "@fluentui/react"; } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as React from "react"; import * as React from "react";
import _ from "underscore"; import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg"; import ChevronDownIcon from "../../../../images/Chevron_down.svg";
@@ -58,11 +57,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
}, },
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => { onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
btn.onCommandClick(ev); btn.onCommandClick(ev);
let copilotEnabled = false; TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label });
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
}
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
}, },
key: `${btn.commandButtonLabel}${index}`, key: `${btn.commandButtonLabel}${index}`,
text: label, text: label,

View File

@@ -114,8 +114,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
super(props); super(props);
this.state = { this.state = {
createNewDatabase: createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "", newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
isSharedThroughputChecked: this.getSharedThroughputDefault(), isSharedThroughputChecked: this.getSharedThroughputDefault(),
selectedDatabaseId: selectedDatabaseId:
@@ -275,38 +274,36 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</TooltipHost> </TooltipHost>
</Stack> </Stack>
{configContext.platform !== Platform.Fabric && ( <Stack horizontal verticalAlign="center">
<Stack horizontal verticalAlign="center"> <div role="radiogroup">
<div role="radiogroup"> <input
<input className="panelRadioBtn"
className="panelRadioBtn" checked={this.state.createNewDatabase}
checked={this.state.createNewDatabase} aria-label="Create new database"
aria-label="Create new database" aria-checked={this.state.createNewDatabase}
aria-checked={this.state.createNewDatabase} name="databaseType"
name="databaseType" type="radio"
type="radio" role="radio"
role="radio" id="databaseCreateNew"
id="databaseCreateNew" tabIndex={0}
tabIndex={0} onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)} />
/> <span className="panelRadioBtnLabel">Create new</span>
<span className="panelRadioBtnLabel">Create new</span>
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.createNewDatabase} checked={!this.state.createNewDatabase}
aria-label="Use existing database" aria-label="Use existing database"
aria-checked={!this.state.createNewDatabase} aria-checked={!this.state.createNewDatabase}
name="databaseType" name="databaseType"
type="radio" type="radio"
role="radio" role="radio"
tabIndex={0} tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)} onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/> />
<span className="panelRadioBtnLabel">Use existing</span> <span className="panelRadioBtnLabel">Use existing</span>
</div> </div>
</Stack> </Stack>
)}
{this.state.createNewDatabase && ( {this.state.createNewDatabase && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
@@ -1431,6 +1428,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.setState({ isExecuting: false }); this.setState({ isExecuting: false });
TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey); TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey);
useSidePanel.getState().closeSidePanel(); useSidePanel.getState().closeSidePanel();
// open NPS Survey Dialog once the collection is created
this.props.explorer.openNPSSurveyDialog();
} catch (error) { } catch (error) {
const errorMessage: string = getErrorMessage(error); const errorMessage: string = getErrorMessage(error);
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true }); this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });

View File

@@ -32,8 +32,14 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
return ( return (
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center"> <Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
{icon} {icon}
<span className="panelWarningErrorDetailsLinkContainer" role="alert" aria-live="assertive"> <span className="panelWarningErrorDetailsLinkContainer">
<Text aria-label={message} className="panelWarningErrorMessage" variant="small"> <Text
role="alert"
aria-live="assertive"
aria-label={message}
className="panelWarningErrorMessage"
variant="small"
>
{message} {message}
{link && linkText && ( {link && linkText && (
<Link target="_blank" href={link}> <Link target="_blank" href={link}>

View File

@@ -1,5 +1,5 @@
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { ReactWrapper, mount } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
import React from "react"; import React from "react";
import { RightPaneForm } from "./RightPaneForm"; import { RightPaneForm } from "./RightPaneForm";
@@ -34,6 +34,6 @@ describe("Right Pane Form", () => {
it("should render error in header", () => { it("should render error in header", () => {
render(<RightPaneForm {...props} formError="file already Exist" />); render(<RightPaneForm {...props} formError="file already Exist" />);
expect(screen.getByLabelText("error")).toBeDefined(); expect(screen.getByLabelText("error")).toBeDefined();
expect(screen.getByRole("alert").innerHTML).toContain("file already Exist"); expect(screen.getByRole("alert").innerHTML).toEqual("file already Exist");
}); });
}); });

View File

@@ -1,13 +1,4 @@
import { import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
Checkbox,
ChoiceGroup,
IChoiceGroupOption,
ISpinButtonStyles,
IToggleStyles,
Position,
SpinButton,
Toggle,
} from "@fluentui/react";
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
@@ -15,10 +6,10 @@ import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as StringUtility from "Shared/StringUtility"; import * as StringUtility from "Shared/StringUtility";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { logConsoleInfo } from "Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, MouseEvent, useState } from "react";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
export const SettingsPane: FunctionComponent = () => { export const SettingsPane: FunctionComponent = () => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
@@ -28,13 +19,6 @@ export const SettingsPane: FunctionComponent = () => {
? Constants.Queries.UnlimitedPageOption ? Constants.Queries.UnlimitedPageOption
: Constants.Queries.CustomPageOption, : Constants.Queries.CustomPageOption,
); );
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
);
const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout));
const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>(
LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout),
);
const [customItemPerPage, setCustomItemPerPage] = useState<number>( const [customItemPerPage, setCustomItemPerPage] = useState<number>(
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0, LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0,
); );
@@ -69,7 +53,7 @@ export const SettingsPane: FunctionComponent = () => {
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin"; const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin"; const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled(); const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
const handlerOnSubmit = () => { const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
setIsExecuting(true); setIsExecuting(true);
LocalStorageUtility.setEntryNumber( LocalStorageUtility.setEntryNumber(
@@ -77,7 +61,6 @@ export const SettingsPane: FunctionComponent = () => {
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage, isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
); );
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
@@ -90,14 +73,6 @@ export const SettingsPane: FunctionComponent = () => {
); );
} }
if (queryTimeoutEnabled) {
LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout);
LocalStorageUtility.setEntryBoolean(
StorageKey.AutomaticallyCancelQueryAfterTimeout,
automaticallyCancelQueryAfterTimeout,
);
}
setIsExecuting(false); setIsExecuting(false);
logConsoleInfo( logConsoleInfo(
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`, `Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
@@ -122,6 +97,7 @@ export const SettingsPane: FunctionComponent = () => {
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`, `Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
); );
closeSidePanel(); closeSidePanel();
e.preventDefault();
}; };
const isCustomPageOptionSelected = () => { const isCustomPageOptionSelected = () => {
@@ -136,7 +112,7 @@ export const SettingsPane: FunctionComponent = () => {
formError: "", formError: "",
isExecuting, isExecuting,
submitButtonText: "Apply", submitButtonText: "Apply",
onSubmit: () => handlerOnSubmit(), onSubmit: () => handlerOnSubmit(undefined),
}; };
const pageOptionList: IChoiceGroupOption[] = [ const pageOptionList: IChoiceGroupOption[] = [
{ key: Constants.Queries.CustomPageOption, text: "Custom" }, { key: Constants.Queries.CustomPageOption, text: "Custom" },
@@ -164,21 +140,6 @@ export const SettingsPane: FunctionComponent = () => {
setPageOption(option.key); setPageOption(option.key);
}; };
const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
setQueryTimeoutEnabled(checked);
};
const handleOnAutomaticallyCancelQueryToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
setAutomaticallyCancelQueryAfterTimeout(checked);
};
const handleOnQueryTimeoutSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
const queryTimeout = Number(newValue);
if (!isNaN(queryTimeout)) {
setQueryTimeout(queryTimeout);
}
};
const choiceButtonStyles = { const choiceButtonStyles = {
root: { root: {
clear: "both", clear: "both",
@@ -200,35 +161,6 @@ export const SettingsPane: FunctionComponent = () => {
}, },
], ],
}; };
const queryTimeoutToggleStyles: IToggleStyles = {
label: {
fontSize: 12,
fontWeight: 400,
display: "block",
},
root: {},
container: {},
pill: {},
thumb: {},
text: {},
};
const queryTimeoutSpinButtonStyles: ISpinButtonStyles = {
label: {
fontSize: 12,
fontWeight: 400,
},
root: {
paddingBottom: 10,
},
labelWrapper: {},
icon: {},
spinButtonWrapper: {},
input: {},
arrowButtonsContainer: {},
};
return ( return (
<RightPaneForm {...genericPaneProps}> <RightPaneForm {...genericPaneProps}>
<div className="paneMainContent"> <div className="paneMainContent">
@@ -279,50 +211,6 @@ export const SettingsPane: FunctionComponent = () => {
</div> </div>
</div> </div>
)} )}
{userContext.apiType === "SQL" && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div>
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
Query Timeout
</legend>
<InfoTooltip>
When a query reaches a specified time limit, a popup with an option to cancel the query will show
unless automatic cancellation has been enabled
</InfoTooltip>
</div>
<div>
<Toggle
styles={queryTimeoutToggleStyles}
label="Enable query timeout"
onChange={handleOnQueryTimeoutToggleChange}
defaultChecked={queryTimeoutEnabled}
/>
</div>
{queryTimeoutEnabled && (
<div>
<SpinButton
label="Query timeout (ms)"
labelPosition={Position.top}
defaultValue={(queryTimeout || 5000).toString()}
min={100}
step={1000}
onChange={handleOnQueryTimeoutSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={queryTimeoutSpinButtonStyles}
/>
<Toggle
label="Automatically cancel query after timeout"
styles={queryTimeoutToggleStyles}
onChange={handleOnAutomaticallyCancelQueryToggleChange}
defaultChecked={automaticallyCancelQueryAfterTimeout}
/>
</div>
)}
</div>
</div>
)}
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart">
<div className="settingsSectionLabel"> <div className="settingsSectionLabel">

View File

@@ -97,46 +97,6 @@ exports[`Settings Pane should render Default properly 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div>
<legend
className="settingsSectionLabel legendLabel"
id="queryTimeoutLabel"
>
Query Timeout
</legend>
<InfoTooltip>
When a query reaches a specified time limit, a popup with an option to cancel the query will show unless automatic cancellation has been enabled
</InfoTooltip>
</div>
<div>
<StyledToggleBase
defaultChecked={false}
label="Enable query timeout"
onChange={[Function]}
styles={
Object {
"container": Object {},
"label": Object {
"display": "block",
"fontSize": 12,
"fontWeight": 400,
},
"pill": Object {},
"root": Object {},
"text": Object {},
"thumb": Object {},
}
}
/>
</div>
</div>
</div>
<div <div
className="settingsSection" className="settingsSection"
> >

View File

@@ -390,9 +390,6 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
</Stack> </Stack>
)} )}
</div> </div>
<div className="panelNullWarning" style={{ padding: "20px", color: "red" }}>
Warning: Null fields will not be displayed for editing.
</div>
</RightPaneForm> </RightPaneForm>
); );
}; };

View File

@@ -355,17 +355,6 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
</div> </div>
</Stack> </Stack>
</div> </div>
<div
className="panelNullWarning"
style={
Object {
"color": "red",
"padding": "20px",
}
}
>
Warning: Null fields will not be displayed for editing.
</div>
<PanelFooterComponent <PanelFooterComponent
buttonLabel="Update" buttonLabel="Update"
isButtonDisabled={false} isButtonDisabled={false}

View File

@@ -336,8 +336,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
directionalHint={4} directionalHint={4}
> >
<Icon <Icon
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
impacting the performance of transactional workloads."
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
tabIndex={0} tabIndex={0}

View File

@@ -323,19 +323,21 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
</IconBase> </IconBase>
</StyledIconBase> </StyledIconBase>
<span <span
aria-live="assertive"
className="panelWarningErrorDetailsLinkContainer" className="panelWarningErrorDetailsLinkContainer"
key=".0:$.1" key=".0:$.1"
role="alert"
> >
<Text <Text
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources." aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
aria-live="assertive"
className="panelWarningErrorMessage" className="panelWarningErrorMessage"
role="alert"
variant="small" variant="small"
> >
<span <span
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources." aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
aria-live="assertive"
className="panelWarningErrorMessage css-56" className="panelWarningErrorMessage css-56"
role="alert"
> >
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources. Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
</span> </span>

View File

@@ -1,10 +1,10 @@
import { Checkbox, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react"; import { Checkbox, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { getUserEmail } from "Utils/UserUtils"; import { getUserEmail } from "Utils/UserUtils";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react"; import React from "react";
jest.mock("Utils/UserUtils"); jest.mock("Utils/UserUtils");
@@ -13,49 +13,21 @@ jest.mock("Utils/UserUtils");
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient"); jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
SubmitFeedback as jest.Mock; SubmitFeedback as jest.Mock;
jest.mock("Explorer/QueryCopilot/QueryCopilotContext");
const mockUseCopilotStore = useCopilotStore as jest.Mock;
const mockReturnValue = {
generatedQuery: "test query",
userPrompt: "test prompt",
likeQuery: false,
showFeedbackModal: false,
closeFeedbackModal: jest.fn,
setHideFeedbackModalForLikedQueries: jest.fn,
};
describe("Query Copilot Feedback Modal snapshot test", () => { describe("Query Copilot Feedback Modal snapshot test", () => {
beforeEach(() => { beforeEach(() => {
mockUseCopilotStore.mockReturnValue(mockReturnValue);
jest.clearAllMocks(); jest.clearAllMocks();
}); });
it("shoud render and match snapshot", () => { it("shoud render and match snapshot", () => {
mockUseCopilotStore.mockReturnValue({ useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
...mockReturnValue,
showFeedbackModal: true, const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
});
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
expect(wrapper.props().isOpen).toBeTruthy(); expect(wrapper.props().isOpen).toBeTruthy();
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("should close on cancel click", () => { it("should close on cancel click", () => {
const wrapper = shallow( const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const cancelButton = wrapper.find(IconButton); const cancelButton = wrapper.find(IconButton);
cancelButton.simulate("click"); cancelButton.simulate("click");
@@ -66,14 +38,7 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
}); });
it("should get user unput", () => { it("should get user unput", () => {
const wrapper = shallow( const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const testUserInput = "test user input"; const testUserInput = "test user input";
const userInput = wrapper.find(TextField).first(); const userInput = wrapper.find(TextField).first();
@@ -83,15 +48,30 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("should record user contact choice no", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
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 explorer={new Explorer()} />);
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", () => { it("should not render dont show again button", () => {
const wrapper = shallow( const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const dontShowAgain = wrapper.find(Checkbox); const dontShowAgain = wrapper.find(Checkbox);
@@ -100,19 +80,8 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
}); });
it("should render dont show again button and check it ", () => { it("should render dont show again button and check it ", () => {
mockUseCopilotStore.mockReturnValue({ useQueryCopilot.getState().openFeedbackModal("test query", true, "test prompt");
...mockReturnValue, const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
showFeedbackModal: true,
likeQuery: true,
});
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const dontShowAgain = wrapper.find(Checkbox); const dontShowAgain = wrapper.find(Checkbox);
dontShowAgain.simulate("change", {}, true); dontShowAgain.simulate("change", {}, true);
@@ -123,14 +92,7 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
}); });
it("should cancel submission", () => { it("should cancel submission", () => {
const wrapper = shallow( const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const cancelButton = wrapper.find(DefaultButton); const cancelButton = wrapper.find(DefaultButton);
cancelButton.simulate("click"); cancelButton.simulate("click");
@@ -140,49 +102,22 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("should not submit submission if required description field is null", () => { it("should submit submission", () => {
const explorer = new Explorer(); const explorer = new Explorer();
const wrapper = shallow( const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
<QueryCopilotFeedbackModal
explorer={explorer}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const submitButton = wrapper.find(PrimaryButton); const submitButton = wrapper.find(PrimaryButton);
submitButton.simulate("click"); submitButton.simulate("click");
wrapper.setProps({}); wrapper.setProps({});
expect(SubmitFeedback).toHaveBeenCalledTimes(0);
});
it("should submit submission", () => {
const explorer = new Explorer();
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={explorer}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const submitButton = wrapper.find("form");
submitButton.simulate("submit");
wrapper.setProps({});
expect(SubmitFeedback).toHaveBeenCalledTimes(1); expect(SubmitFeedback).toHaveBeenCalledTimes(1);
expect(SubmitFeedback).toHaveBeenCalledWith({ expect(SubmitFeedback).toHaveBeenCalledWith({
containerId: "CopilotUserContainer",
databaseId: "CopilotUserDb",
mode: "User",
params: { params: {
likeQuery: false, likeQuery: false,
generatedQuery: "test query", generatedQuery: "",
userPrompt: "test prompt", userPrompt: "",
description: "", description: "",
contact: getUserEmail(),
}, },
explorer: explorer, explorer: explorer,
}); });

View File

@@ -1,5 +1,6 @@
import { import {
Checkbox, Checkbox,
ChoiceGroup,
DefaultButton, DefaultButton,
IconButton, IconButton,
Link, Link,
@@ -10,21 +11,12 @@ import {
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react"; import React from "react";
import { getUserEmail } from "../../../Utils/UserUtils";
export const QueryCopilotFeedbackModal = ({ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }): JSX.Element => {
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): JSX.Element => {
const { const {
generatedQuery, generatedQuery,
userPrompt, userPrompt,
@@ -32,72 +24,94 @@ export const QueryCopilotFeedbackModal = ({
showFeedbackModal, showFeedbackModal,
closeFeedbackModal, closeFeedbackModal,
setHideFeedbackModalForLikedQueries, setHideFeedbackModalForLikedQueries,
} = useCopilotStore(); } = useQueryCopilot();
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(true);
const [description, setDescription] = React.useState<string>(""); const [description, setDescription] = React.useState<string>("");
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false); const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
const [contact, setContact] = React.useState<string>(getUserEmail());
const handleSubmit = () => {
closeFeedbackModal();
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
SubmitFeedback({
params: { generatedQuery, likeQuery, description, userPrompt },
explorer,
databaseId,
containerId,
mode: mode,
});
};
return ( return (
<Modal isOpen={showFeedbackModal}> <Modal isOpen={showFeedbackModal}>
<form onSubmit={handleSubmit}> <Stack style={{ padding: 24 }}>
<Stack style={{ padding: 24 }}> <Stack horizontal horizontalAlign="space-between">
<Stack horizontal horizontalAlign="space-between"> <Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text> <IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
</Stack>
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
<TextField
styles={{ root: { marginBottom: 14 } }}
label="Description"
required
placeholder="Provide more details"
value={description}
onChange={(_, newValue) => setDescription(newValue)}
multiline
rows={3}
/>
<TextField
styles={{ root: { marginBottom: 14 } }}
label="Query generated"
defaultValue={generatedQuery}
readOnly
/>
<Text style={{ fontSize: 12, marginBottom: 14 }}>
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
{
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
Privacy statement
</Link>
}{" "}
for more information.
</Text>
{likeQuery && (
<Checkbox
styles={{ label: { paddingLeft: 0 }, root: { marginBottom: 14 } }}
label="Don't show me this next time"
checked={doNotShowAgainChecked}
onChange={(_, checked) => setDoNotShowAgainChecked(checked)}
/>
)}
<Stack horizontal horizontalAlign="end">
<PrimaryButton styles={{ root: { marginRight: 8 } }} type="submit">
Submit
</PrimaryButton>
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
</Stack>
</Stack> </Stack>
</form> <Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
<TextField
styles={{ root: { marginBottom: 14 } }}
label="Description"
required
placeholder="Provide more details"
value={description}
onChange={(_, newValue) => setDescription(newValue)}
multiline
rows={3}
/>
<TextField
styles={{ root: { marginBottom: 14 } }}
label="Query generated"
defaultValue={generatedQuery}
readOnly
/>
<ChoiceGroup
styles={{
root: {
marginBottom: 14,
},
flexContainer: {
selectors: {
".ms-ChoiceField-field::before": { marginTop: 4 },
".ms-ChoiceField-field::after": { marginTop: 4 },
".ms-ChoiceFieldLabel": { paddingLeft: 6 },
},
},
}}
label="May we contact you about your feedback?"
options={[
{ key: "yes", text: "Yes, you may contact me." },
{ key: "no", text: "No, do not contact me." },
]}
selectedKey={isContactAllowed ? "yes" : "no"}
onChange={(_, option) => {
setIsContactAllowed(option.key === "yes");
setContact(option.key === "yes" ? getUserEmail() : "");
}}
></ChoiceGroup>
<Text style={{ fontSize: 12, marginBottom: 14 }}>
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
{
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
Privacy statement
</Link>
}{" "}
for more information.
</Text>
{likeQuery && (
<Checkbox
styles={{ label: { paddingLeft: 0 }, root: { marginBottom: 14 } }}
label="Don't show me this next time"
checked={doNotShowAgainChecked}
onChange={(_, checked) => setDoNotShowAgainChecked(checked)}
/>
)}
<Stack horizontal horizontalAlign="end">
<PrimaryButton
styles={{ root: { marginRight: 8 } }}
onClick={() => {
closeFeedbackModal();
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
SubmitFeedback({
params: { generatedQuery, likeQuery, description, userPrompt, contact },
explorer: explorer,
});
}}
>
Submit
</PrimaryButton>
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
</Stack>
</Stack>
</Modal> </Modal>
); );
}; };

View File

@@ -1,6 +1,7 @@
.modalContentPadding { .modalContentPadding {
padding-top: 15px; padding-top: 15px;
width: 513px; width: 513px;
height: 638px;
} }
.exitPadding { .exitPadding {
@@ -38,7 +39,7 @@
} }
.buttonPadding { .buttonPadding {
padding: 15px 0px 15px 0px; padding: 15px 0px 0px 0px;
} }
.tryButton { .tryButton {

View File

@@ -1,7 +1,7 @@
import { IconButton, Image, Link, Modal, PrimaryButton, Stack, StackItem, Text } from "@fluentui/react"; import { IconButton, Image, Link, Modal, PrimaryButton, Stack, StackItem, Text } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react"; import React from "react";
import Database from "../../../../images/CopilotDatabase.svg";
import Flash from "../../../../images/CopilotFlash.svg"; import Flash from "../../../../images/CopilotFlash.svg";
import Thumb from "../../../../images/CopilotThumb.svg"; import Thumb from "../../../../images/CopilotThumb.svg";
import CoplilotWelcomeIllustration from "../../../../images/CopliotWelcomeIllustration.svg"; import CoplilotWelcomeIllustration from "../../../../images/CopliotWelcomeIllustration.svg";
@@ -14,11 +14,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
if (visible) { if (visible) {
window.localStorage.setItem("hideWelcomeModal", "true"); window.localStorage.setItem("hideWelcomeModal", "true");
} }
}, []); });
React.useEffect(() => {
useQueryCopilot.getState().setShowWelcomeModal(isModalVisible);
}, [isModalVisible]);
return ( return (
<> <>
@@ -27,10 +23,8 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
onDismiss={hideModal} onDismiss={hideModal}
isBlocking={false} isBlocking={false}
styles={{ styles={{
main: { scrollableContent: {
maxHeight: 600, minHeight: 680,
borderRadius: 10,
overflow: "hidden",
}, },
}} }}
> >
@@ -58,7 +52,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</Stack> </Stack>
<Stack horizontalAlign="center"> <Stack horizontalAlign="center">
<Stack.Item align="center" style={{ textAlign: "center" }}> <Stack.Item align="center" style={{ textAlign: "center" }}>
<Text className="title bold">Welcome to Microsoft Copilot for Azure in Cosmos DB (preview)</Text> <Text className="title bold">Welcome to Copilot in Azure Cosmos DB (Private Preview)</Text>
</Stack.Item> </Stack.Item>
<Stack.Item align="center" className="text"> <Stack.Item align="center" className="text">
<Stack horizontal> <Stack horizontal>
@@ -75,7 +69,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text> <Text>
Ask Copilot to generate a query by describing the query in your words. Ask Copilot to generate a query by describing the query in your words.
<br /> <br />
<Link target="_blank" href="https://aka.ms/MicrosoftCopilotForAzureInCDBHowTo"> <Link target="_blank" href="https://aka.ms/cdb-copilot-learn-more">
Learn more Learn more
</Link> </Link>
</Text> </Text>
@@ -93,11 +87,31 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</StackItem> </StackItem>
</Stack> </Stack>
<Text> <Text>
AI-generated content can have mistakes. Make sure it is accurate and appropriate before executing the AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
query.
<br /> <br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-preview-terms"> <Link target="_blank" href="https://aka.ms/cdb-copilot-preview-terms">
Read our preview terms here Read preview terms
</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
<Stack horizontal>
<StackItem align="start" className="imageTextPadding">
<Image src={Database} />
</StackItem>
<StackItem align="start">
<Text className="bold">
Query Copilot works on a sample database.
<br />
</Text>
</StackItem>
</Stack>
<Text>
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you
at no cost.
<br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-learn-more">
Learn more
</Link> </Link>
</Text> </Text>
</Stack.Item> </Stack.Item>

View File

@@ -8,10 +8,8 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
onDismiss={[Function]} onDismiss={[Function]}
styles={ styles={
Object { Object {
"main": Object { "scrollableContent": Object {
"borderRadius": 10, "minHeight": 680,
"maxHeight": 600,
"overflow": "hidden",
}, },
} }
} }
@@ -78,7 +76,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
<Text <Text
className="title bold" className="title bold"
> >
Welcome to Microsoft Copilot for Azure in Cosmos DB (preview) Welcome to Copilot in Azure Cosmos DB (Private Preview)
</Text> </Text>
</StackItem> </StackItem>
<StackItem <StackItem
@@ -111,7 +109,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
Ask Copilot to generate a query by describing the query in your words. Ask Copilot to generate a query by describing the query in your words.
<br /> <br />
<StyledLinkBase <StyledLinkBase
href="https://aka.ms/MicrosoftCopilotForAzureInCDBHowTo" href="https://aka.ms/cdb-copilot-learn-more"
target="_blank" target="_blank"
> >
Learn more Learn more
@@ -145,13 +143,50 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
</StackItem> </StackItem>
</Stack> </Stack>
<Text> <Text>
AI-generated content can have mistakes. Make sure it is accurate and appropriate before executing the query. AI-generated content can have mistakes. Make sure its accurate and appropriate before using it.
<br /> <br />
<StyledLinkBase <StyledLinkBase
href="https://aka.ms/cdb-copilot-preview-terms" href="https://aka.ms/cdb-copilot-preview-terms"
target="_blank" target="_blank"
> >
Read our preview terms here Read preview terms
</StyledLinkBase>
</Text>
</StackItem>
<StackItem
align="center"
className="text"
>
<Stack
horizontal={true}
>
<StackItem
align="start"
className="imageTextPadding"
>
<Image
src={Object {}}
/>
</StackItem>
<StackItem
align="start"
>
<Text
className="bold"
>
Query Copilot works on a sample database.
<br />
</Text>
</StackItem>
</Stack>
<Text>
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you at no cost.
<br />
<StyledLinkBase
href="https://aka.ms/cdb-copilot-learn-more"
target="_blank"
>
Learn more
</StyledLinkBase> </StyledLinkBase>
</Text> </Text>
</StackItem> </StackItem>

View File

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

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */ /* eslint-disable no-console */
import { import {
Callout, Callout,
@@ -19,61 +18,50 @@ import {
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { HttpStatusCodes } from "Common/Constants"; import {
ContainerStatusType,
PoolIdType,
QueryCopilotSampleContainerSchema,
ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal"; import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup"; import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup"; import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
import { import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
SuggestedPrompt,
getSampleDatabaseSuggestedPrompts,
getSuggestedPrompts,
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts"; import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import HintIcon from "../../../images/Hint.svg"; import HintIcon from "../../../images/Hint.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import RecentIcon from "../../../images/Recent.svg"; import RecentIcon from "../../../images/Recent.svg";
import errorIcon from "../../../images/close-black.svg"; import errorIcon from "../../../images/close-black.svg";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { useCopilotStore } from "../QueryCopilot/QueryCopilotContext";
import { useSelectedNode } from "../useSelectedNode";
type QueryCopilotPromptProps = QueryCopilotProps & { type QueryCopilotPromptProps = QueryCopilotProps & {
databaseId: string;
containerId: string;
toggleCopilot: (toggle: boolean) => void; toggleCopilot: (toggle: boolean) => void;
}; };
interface SuggestedPrompt {
id: number;
text: string;
}
const promptStyles: IButtonStyles = { const promptStyles: IButtonStyles = {
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } }, root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
label: { label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
fontWeight: 400,
textAlign: "left",
paddingLeft: 8,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
},
textContainer: { overflow: "hidden" },
}; };
export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
explorer, explorer,
toggleCopilot, toggleCopilot,
databaseId,
containerId,
}: QueryCopilotPromptProps): JSX.Element => { }: QueryCopilotPromptProps): JSX.Element => {
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false); const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
const inputEdited = useRef(false);
const { const {
openFeedbackModal,
hideFeedbackModalForLikedQueries, hideFeedbackModalForLikedQueries,
userPrompt, userPrompt,
setUserPrompt, setUserPrompt,
@@ -106,10 +94,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setGeneratedQueryComments, setGeneratedQueryComments,
setQueryResults, setQueryResults,
setErrorMessage, setErrorMessage,
errorMessage, } = useQueryCopilot();
} = useCopilotStore();
const inputEdited = useRef(!!userPrompt);
const sampleProps: SamplePromptsProps = { const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen, isSamplePromptsOpen: isSamplePromptsOpen,
@@ -134,13 +119,14 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}, 6000); }, 6000);
}; };
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`); const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
const cachedHistories = cachedHistoriesString?.split("|"); const cachedHistories = cachedHistoriesString?.split("|");
const [histories, setHistories] = useState<string[]>(cachedHistories || []); const [histories, setHistories] = useState<string[]>(cachedHistories || []);
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive const suggestedPrompts: SuggestedPrompt[] = [
? getSampleDatabaseSuggestedPrompts() { id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
: getSuggestedPrompts(); { 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 [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts); const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
@@ -155,7 +141,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
// Filter suggested prompts // Filter suggested prompts
const filteredSuggested = suggestedPrompts.filter((prompt) => const filteredSuggested = suggestedPrompts.filter((prompt) =>
prompt.text.toLowerCase().includes(value.toLowerCase()), prompt.text.toLowerCase().includes(value.toLowerCase())
); );
setFilteredSuggestedPrompts(filteredSuggested); setFilteredSuggestedPrompts(filteredSuggested);
}; };
@@ -165,7 +151,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim()); const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
const updatedHistories = existingHistories.filter( const updatedHistories = existingHistories.filter(
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase(), (history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase()
); );
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)]; const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
@@ -191,24 +177,28 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setShowDeletePopup(false); setShowDeletePopup(false);
useTabs.getState().setIsTabExecuting(true); useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false); useTabs.getState().setIsQueryErrorThrown(false);
const mode: string = isSampleCopilotActive ? "Sample" : "User"; if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
await allocatePhoenixContainer({ explorer, databaseId, containerId, mode }); !userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot);
}
const payload = { const payload = {
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
userPrompt: userPrompt, userPrompt: userPrompt,
}; };
useQueryCopilot.getState().refreshCorrelationId(); useQueryCopilot.getState().refreshCorrelationId();
const serverInfo = useQueryCopilot.getState().notebookServerInfo; const serverInfo = useQueryCopilot.getState().notebookServerInfo;
const queryUri = userContext.features.disableCopilotPhoenixGateaway const queryUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery") ? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery")
: createUri(serverInfo.notebookServerEndpoint, "public/generateSQLQuery"); : createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery");
const response = await fetch(queryUri, { const response = await fetch(queryUri, {
method: "POST", method: "POST",
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId, "x-ms-correlationid": useQueryCopilot.getState().correlationId,
Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`,
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
@@ -226,40 +216,13 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setGeneratedQueryComments(generateSQLQueryResponse.explanation); setGeneratedQueryComments(generateSQLQueryResponse.explanation);
setShowFeedbackBar(true); setShowFeedbackBar(true);
resetQueryResults(); resetQueryResults();
TelemetryProcessor.traceSuccess(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
copilotLatency:
Date.parse(generateSQLQueryResponse?.generateEnd) - Date.parse(generateSQLQueryResponse?.generateStart),
responseCode: response.status,
});
} else { } else {
setShowInvalidQueryMessageBar(true); setShowInvalidQueryMessageBar(true);
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
responseCode: response.status,
});
} }
} else if (response?.status === HttpStatusCodes.TooManyRequests) {
handleError(JSON.stringify(generateSQLQueryResponse), "copilotTooManyRequestError");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
setErrorMessage("Ratelimit exceeded 5 per 1 minute. Please try again after sometime");
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
responseCode: response.status,
});
} else { } else {
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError"); handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
useTabs.getState().setIsQueryErrorThrown(true); useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true); setShowErrorMessageBar(true);
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
responseCode: response.status,
});
} }
} catch (error) { } catch (error) {
handleError(error, "executeNaturalLanguageQuery"); handleError(error, "executeNaturalLanguageQuery");
@@ -275,7 +238,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
const showTeachingBubble = (): void => { const showTeachingBubble = (): void => {
if (!inputEdited.current) { if (!inputEdited.current) {
setTimeout(() => { setTimeout(() => {
if (!useQueryCopilot.getState().showWelcomeModal && !userPrompt && !inputEdited.current) { if (!inputEdited.current) {
toggleCopilotTeachingBubbleVisible(); toggleCopilotTeachingBubbleVisible();
inputEdited.current = true; inputEdited.current = true;
} }
@@ -302,7 +265,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
React.useEffect(() => { React.useEffect(() => {
showTeachingBubble(); showTeachingBubble();
useQueryCopilot.subscribe(showTeachingBubble, (state: QueryCopilotState) => state.showWelcomeModal);
useTabs.getState().setIsQueryErrorThrown(false); useTabs.getState().setIsQueryErrorThrown(false);
}, []); }, []);
@@ -336,7 +298,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setShowSamplePrompts(true); setShowSamplePrompts(true);
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && userPrompt) { if (e.key === "Enter") {
inputEdited.current = true; inputEdited.current = true;
startGenerateQueryProcess(); startGenerateQueryProcess();
} }
@@ -345,7 +307,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }} styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }}
disabled={isGeneratingQuery} disabled={isGeneratingQuery}
autoComplete="off" autoComplete="off"
placeholder="Ask a question in natural language and well generate the query for you."
/> />
{copilotTeachingBubbleVisible && ( {copilotTeachingBubbleVisible && (
<TeachingBubble <TeachingBubble
@@ -379,7 +340,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />} {isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
{showSamplePrompts && ( {showSamplePrompts && (
<Callout <Callout
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }} styles={{ root: { minWidth: 400 } }}
target="#naturalLanguageInput" target="#naturalLanguageInput"
isBeakVisible={false} isBeakVisible={false}
onDismiss={() => setShowSamplePrompts(false)} onDismiss={() => setShowSamplePrompts(false)}
@@ -411,7 +372,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setShowSamplePrompts(false); setShowSamplePrompts(false);
inputEdited.current = true; inputEdited.current = true;
}} }}
onRenderIcon={() => <Image src={RecentIcon} styles={{ root: { overflow: "unset" } }} />} onRenderIcon={() => <Image src={RecentIcon} />}
styles={promptStyles} styles={promptStyles}
> >
{history} {history}
@@ -487,7 +448,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
</Link> </Link>
{showErrorMessageBar && ( {showErrorMessageBar && (
<MessageBar messageBarType={MessageBarType.error}> <MessageBar messageBarType={MessageBarType.error}>
{errorMessage ? errorMessage : "We ran into an error and were not able to execute query."} We ran into an error and were not able to execute query.
</MessageBar> </MessageBar>
)} )}
{showInvalidQueryMessageBar && ( {showInvalidQueryMessageBar && (
@@ -525,10 +486,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
description: "", description: "",
userPrompt: userPrompt, userPrompt: userPrompt,
}, },
explorer, explorer: explorer,
databaseId,
containerId,
mode: isSampleCopilotActive ? "Sample" : "User",
}); });
}} }}
directionalHint={DirectionalHint.topCenter} directionalHint={DirectionalHint.topCenter}
@@ -538,7 +496,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<Link <Link
onClick={() => { onClick={() => {
setShowCallout(false); setShowCallout(false);
openFeedbackModal(generatedQuery, true, userPrompt); useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
}} }}
> >
more feedback? more feedback?
@@ -563,7 +521,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }} iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => { onClick={() => {
if (!dislikeQuery) { if (!dislikeQuery) {
openFeedbackModal(generatedQuery, false, userPrompt); useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
setLikeQuery(false); setLikeQuery(false);
} }
setDislikeQuery(!dislikeQuery); setDislikeQuery(!dislikeQuery);
@@ -576,7 +534,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
iconProps={{ iconName: "Copy" }} iconProps={{ iconName: "Copy" }}
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }} style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
> >
Copy query Copy code
</CommandBarButton> </CommandBarButton>
<CommandBarButton <CommandBarButton
onClick={() => { onClick={() => {
@@ -585,11 +543,11 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
iconProps={{ iconName: "Delete" }} iconProps={{ iconName: "Delete" }}
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }} style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
> >
Delete query Delete code
</CommandBarButton> </CommandBarButton>
</Stack> </Stack>
)} )}
<WelcomeModal visible={useQueryCopilot.getState().showWelcomeModal} /> <WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />} {isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
{query !== "" && query.trim().length !== 0 && ( {query !== "" && query.trim().length !== 0 && (
<DeletePopup <DeletePopup

View File

@@ -1,6 +1,5 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { Stack } from "@fluentui/react"; import { Stack } from "@fluentui/react";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@@ -12,7 +11,6 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
import React, { useState } from "react"; import React, { useState } from "react";
import SplitterLayout from "react-splitter-layout"; import SplitterLayout from "react-splitter-layout";
import QueryCommandIcon from "../../../images/CopilotCommand.svg"; import QueryCommandIcon from "../../../images/CopilotCommand.svg";
@@ -23,21 +21,15 @@ import * as StringUtility from "../../Shared/StringUtility";
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => { export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot(); const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
const cachedCopilotToggleStatus: string = localStorage.getItem( const cachedCopilotToggleStatus = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`);
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, const [copilotActive, setCopilotActive] = useState<boolean>(StringUtility.toBoolean(cachedCopilotToggleStatus));
);
const copilotInitialActive: boolean = cachedCopilotToggleStatus
? StringUtility.toBoolean(cachedCopilotToggleStatus)
: true;
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
const [tabActive, setTabActive] = useState<boolean>(true);
const getCommandbarButtons = (): CommandButtonComponentProps[] => { const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query"; const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
const executeQueryBtn = { const executeQueryBtn = {
iconSrc: ExecuteQueryIcon, iconSrc: ExecuteQueryIcon,
iconAlt: executeQueryBtnLabel, iconAlt: executeQueryBtnLabel,
onCommandClick: () => OnExecuteQueryClick(useQueryCopilot), onCommandClick: () => OnExecuteQueryClick(),
commandButtonLabel: executeQueryBtnLabel, commandButtonLabel: executeQueryBtnLabel,
ariaLabel: executeQueryBtnLabel, ariaLabel: executeQueryBtnLabel,
hasPopup: false, hasPopup: false,
@@ -76,13 +68,10 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
useTabs.subscribe((state: TabsState) => { const commandbarButtons = getCommandbarButtons();
if (state.activeReactTab === ReactTabKind.QueryCopilot) { commandbarButtons.pop();
setTabActive(true); commandbarButtons.map((props: CommandButtonComponentProps) => (props.disabled = true));
} else { useCommandBar.getState().setContextButtons(commandbarButtons);
setTabActive(false);
}
});
}; };
}, []); }, []);
@@ -94,16 +83,11 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
return ( return (
<Stack className="tab-pane" style={{ width: "100%" }}> <Stack className="tab-pane" style={{ width: "100%" }}>
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}> <div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
{tabActive && copilotActive && ( {copilotActive && (
<QueryCopilotPromptbar <QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar>
explorer={explorer}
toggleCopilot={toggleCopilot}
databaseId={QueryCopilotSampleDatabaseId}
containerId={QueryCopilotSampleContainerId}
></QueryCopilotPromptbar>
)} )}
<Stack className="tabPaneContentContainer"> <Stack className="tabPaneContentContainer">
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}> <SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
<EditorReact <EditorReact
language={"sql"} language={"sql"}
content={query} content={query}

View File

@@ -7,11 +7,6 @@ import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
import DocumentId from "Explorer/Tree/DocumentId"; import DocumentId from "Explorer/Tree/DocumentId";
import { logConsoleProgress } from "Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
export interface SuggestedPrompt {
id: number;
text: string;
}
export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => { export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
options = getCommonQueryOptions(options); options = getCommonQueryOptions(options);
return sampleDataClient() return sampleDataClient()
@@ -38,19 +33,3 @@ export const readSampleDocument = async (documentId: DocumentId): Promise<Item>
clearMessage(); clearMessage();
} }
}; };
export const getSampleDatabaseSuggestedPrompts = (): SuggestedPrompt[] => {
return [
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
];
};
export const getSuggestedPrompts = (): SuggestedPrompt[] => {
return [
{ id: 1, text: "Show the first 10 items" },
{ id: 2, text: 'Count all the items in my data as "numItems"' },
{ id: 3, text: "Find the oldest item added to my collection" },
];
};

View File

@@ -1,3 +1,4 @@
import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
@@ -36,6 +37,9 @@ describe("Query Copilot Client", () => {
userPrompt: "UserPrompt", userPrompt: "UserPrompt",
description: "Description", description: "Description",
contact: "Contact", contact: "Contact",
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
}; };
const mockStore = useQueryCopilot.getState(); const mockStore = useQueryCopilot.getState();
@@ -48,16 +52,13 @@ describe("Query Copilot Client", () => {
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "feedback") ? createUri("https://copilotorchestrater.azurewebsites.net/", "feedback")
: createUri(useQueryCopilot.getState().notebookServerInfo.notebookServerEndpoint, "public/feedback"); : createUri(useQueryCopilot.getState().notebookServerInfo.notebookServerEndpoint, "feedback");
it("should call fetch with the payload with like", async () => { it("should call fetch with the payload with like", async () => {
const mockFetch = jest.fn().mockResolvedValueOnce({}); const mockFetch = jest.fn().mockResolvedValueOnce({});
globalThis.fetch = mockFetch; globalThis.fetch = mockFetch;
await SubmitFeedback({ await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: { params: {
likeQuery: true, likeQuery: true,
generatedQuery: "GeneratedQuery", generatedQuery: "GeneratedQuery",
@@ -90,9 +91,6 @@ describe("Query Copilot Client", () => {
globalThis.fetch = mockFetch; globalThis.fetch = mockFetch;
await SubmitFeedback({ await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: { params: {
likeQuery: false, likeQuery: false,
generatedQuery: "GeneratedQuery", generatedQuery: "GeneratedQuery",
@@ -110,7 +108,6 @@ describe("Query Copilot Client", () => {
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"x-ms-correlationid": "mocked-correlation-id", "x-ms-correlationid": "mocked-correlation-id",
Authorization: "token mocked-token",
}, },
}), }),
); );
@@ -123,9 +120,6 @@ describe("Query Copilot Client", () => {
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error")); globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
await SubmitFeedback({ await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: { params: {
likeQuery: true, likeQuery: true,
generatedQuery: "GeneratedQuery", generatedQuery: "GeneratedQuery",

View File

@@ -1,192 +1,28 @@
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { import {
Areas,
ConnectionStatusType,
ContainerStatusType, ContainerStatusType,
HttpStatusCodes,
PoolIdType, PoolIdType,
QueryCopilotSampleContainerId, QueryCopilotSampleContainerId,
QueryCopilotSampleContainerSchema, QueryCopilotSampleContainerSchema,
ShortenedQueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants"; } from "Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "Common/ErrorHandlingUtils"; import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility"; import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
import { MinimalQueryIterator } from "Common/IteratorUtilities"; import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { configContext } from "ConfigContext"; import { QueryResults } from "Contracts/ViewModels";
import {
ContainerConnectionInfo,
CopilotEnabledConfiguration,
FeatureRegistration,
IProvisionData,
} from "Contracts/DataModels";
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
import { useDialog } from "Explorer/Controls/Dialog";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
import * as StringUtility from "../../../Shared/StringUtility"; import * as StringUtility from "../../../Shared/StringUtility";
async function fetchWithTimeout(
url: string,
headers: {
[x: string]: string;
},
) {
const timeout = 10000;
const options = { timeout };
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await window.fetch(url, {
headers,
...options,
signal: controller.signal,
});
clearTimeout(id);
return response;
}
export const isCopilotFeatureRegistered = async (subscriptionId: string): Promise<boolean> => {
const api_version = "2021-07-01";
const url = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.Features/featureProviders/Microsoft.DocumentDB/subscriptionFeatureRegistrations/MicrosoftCopilotForAzureInCDB?api-version=${api_version}`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
let response;
try {
response = await fetchWithTimeout(url, headers);
} catch (error) {
return false;
}
if (!response?.ok) {
return false;
}
const featureRegistration = (await response?.json()) as FeatureRegistration;
return featureRegistration?.properties?.state === "Registered";
};
export const getCopilotEnabled = async (): Promise<boolean> => {
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
let response;
try {
response = await fetchWithTimeout(url, headers);
} catch (error) {
return false;
}
if (!response?.ok) {
return false;
}
const copilotPortalConfiguration = (await response?.json()) as CopilotEnabledConfiguration;
return copilotPortalConfiguration?.isEnabled;
};
export const allocatePhoenixContainer = async ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => {
try {
if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot, mode);
} else {
const currentAllocatedSchemaInfo = useQueryCopilot.getState().schemaAllocationInfo;
if (
currentAllocatedSchemaInfo.databaseId !== databaseId ||
currentAllocatedSchemaInfo.containerId !== containerId
) {
await resetPhoenixContainerSchema({ explorer, databaseId, containerId, mode });
}
}
useQueryCopilot.getState().setSchemaAllocationInfo({
databaseId,
containerId,
});
} catch (error) {
traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Copilot,
status: error.status,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
useQueryCopilot.getState().resetContainerConnection();
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
} else {
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
);
}
} finally {
useTabs.getState().setIsTabExecuting(false);
}
};
export const resetPhoenixContainerSchema = async ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => {
try {
const provisionData: IProvisionData = {
poolId: PoolIdType.QueryCopilot,
databaseId: databaseId,
containerId: containerId,
mode: mode,
};
const connectionInfo = await explorer.phoenixClient.allocateContainer(provisionData);
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
await explorer.setNotebookInfo(false, connectionInfo, connectionStatus);
} catch (error) {
traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Copilot,
status: error.status,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
throw error;
}
};
export const SendQueryRequest = async ({ export const SendQueryRequest = async ({
userPrompt, userPrompt,
explorer, explorer,
@@ -215,7 +51,7 @@ export const SendQueryRequest = async ({
const queryUri = userContext.features.disableCopilotPhoenixGateaway const queryUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery") ? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery")
: createUri(serverInfo.notebookServerEndpoint, "public/generateSQLQuery"); : createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery");
const payload = { const payload = {
containerSchema: userContext.features.enableCopilotFullSchema containerSchema: userContext.features.enableCopilotFullSchema
@@ -270,19 +106,16 @@ export const SendQueryRequest = async ({
export const SubmitFeedback = async ({ export const SubmitFeedback = async ({
params, params,
explorer, explorer,
databaseId,
containerId,
mode,
}: { }: {
params: FeedbackParams; params: FeedbackParams;
explorer: Explorer; explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => { }): Promise<void> => {
try { try {
const { likeQuery, generatedQuery, userPrompt, description, contact } = params; const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
const payload = { const payload = {
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
like: likeQuery ? "like" : "dislike", like: likeQuery ? "like" : "dislike",
generatedSql: generatedQuery, generatedSql: generatedQuery,
userPrompt, userPrompt,
@@ -293,18 +126,17 @@ export const SubmitFeedback = async ({
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active && useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway !userContext.features.disableCopilotPhoenixGateaway
) { ) {
await allocatePhoenixContainer({ explorer, databaseId, containerId, mode }); await explorer.allocateContainer(PoolIdType.QueryCopilot);
} }
const serverInfo = useQueryCopilot.getState().notebookServerInfo; const serverInfo = useQueryCopilot.getState().notebookServerInfo;
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
? createUri("https://copilotorchestrater.azurewebsites.net/", "feedback") ? createUri("https://copilotorchestrater.azurewebsites.net/", "feedback")
: createUri(serverInfo.notebookServerEndpoint, "public/feedback"); : createUri(serverInfo.notebookServerEndpoint, "feedback");
await fetch(feedbackUri, { await fetch(feedbackUri, {
method: "POST", method: "POST",
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId, "x-ms-correlationid": useQueryCopilot.getState().correlationId,
Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`,
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
@@ -313,7 +145,7 @@ export const SubmitFeedback = async ({
} }
}; };
export const OnExecuteQueryClick = async (useQueryCopilot: Partial<QueryCopilotState>): Promise<void> => { export const OnExecuteQueryClick = async (): Promise<void> => {
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, { traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId, correlationId: useQueryCopilot.getState().correlationId,
userPrompt: useQueryCopilot.getState().userPrompt, userPrompt: useQueryCopilot.getState().userPrompt,
@@ -328,14 +160,13 @@ export const OnExecuteQueryClick = async (useQueryCopilot: Partial<QueryCopilotS
useQueryCopilot.getState().setQueryIterator(queryIterator); useQueryCopilot.getState().setQueryIterator(queryIterator);
setTimeout(async () => { setTimeout(async () => {
await QueryDocumentsPerPage(0, queryIterator, useQueryCopilot); await QueryDocumentsPerPage(0, queryIterator);
}, 100); }, 100);
}; };
export const QueryDocumentsPerPage = async ( export const QueryDocumentsPerPage = async (
firstItemIndex: number, firstItemIndex: number,
queryIterator: MinimalQueryIterator, queryIterator: MinimalQueryIterator
useQueryCopilot: Partial<QueryCopilotState>,
): Promise<void> => { ): Promise<void> => {
try { try {
useQueryCopilot.getState().setIsExecuting(true); useQueryCopilot.getState().setIsExecuting(true);
@@ -343,8 +174,7 @@ export const QueryDocumentsPerPage = async (
useTabs.getState().setIsQueryErrorThrown(false); useTabs.getState().setIsQueryErrorThrown(false);
const queryResults: QueryResults = await queryPagesUntilContentPresent( const queryResults: QueryResults = await queryPagesUntilContentPresent(
firstItemIndex, firstItemIndex,
async (firstItemIndex: number) => async (firstItemIndex: number) => queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex),
); );
useQueryCopilot.getState().setQueryResults(queryResults); useQueryCopilot.getState().setQueryResults(queryResults);
@@ -355,7 +185,7 @@ export const QueryDocumentsPerPage = async (
}); });
} catch (error) { } catch (error) {
const isCopilotActive = StringUtility.toBoolean( const isCopilotActive = StringUtility.toBoolean(
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`), localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`)
); );
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, { traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {

View File

@@ -32,8 +32,3 @@ export interface FeedbackParams {
export interface QueryCopilotProps { export interface QueryCopilotProps {
explorer: Explorer; explorer: Explorer;
} }
export interface CopilotSchemaAllocationInfo {
databaseId: string;
containerId: string;
}

View File

@@ -12,7 +12,7 @@ export const QueryCopilotResults: React.FC = (): JSX.Element => {
queryResults={useQueryCopilot.getState().queryResults} queryResults={useQueryCopilot.getState().queryResults}
isExecuting={useQueryCopilot.getState().isExecuting} isExecuting={useQueryCopilot.getState().isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) => executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator, useQueryCopilot) QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator)
} }
/> />
); );

View File

@@ -17,39 +17,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
} }
} }
> >
<QueryCopilotPromptbar
containerId="SampleContainer"
databaseId="CopilotSampleDb"
explorer={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
}
toggleCopilot={[Function]}
/>
<Stack <Stack
className="tabPaneContentContainer" className="tabPaneContentContainer"
> >
@@ -58,10 +25,10 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
onDragEnd={null} onDragEnd={null}
onDragStart={null} onDragStart={null}
onSecondaryPaneSizeChange={null} onSecondaryPaneSizeChange={null}
percentage={true} percentage={false}
primaryIndex={0} primaryIndex={0}
primaryMinSize={30} primaryMinSize={100}
secondaryMinSize={70} secondaryMinSize={200}
vertical={true} vertical={true}
> >
<EditorReact <EditorReact

View File

@@ -1,6 +1,5 @@
export const newDbAndCollectionCommand = `use quickstartDB export const newDbAndCollectionCommand = `use quickstartDB
db.createCollection('sampleCollection') db.createCollection('sampleCollection')`;
`;
export const newDbAndCollectionCommandForDisplay = `use quickstartDB // Create new database named 'quickstartDB' or switch to it if it already exists export const newDbAndCollectionCommandForDisplay = `use quickstartDB // Create new database named 'quickstartDB' or switch to it if it already exists
@@ -17,25 +16,19 @@ export const loadDataCommand = `db.sampleCollection.insertMany([
{title: "War and Peace", author: "Leo Tolstoy", pages: 1392}, {title: "War and Peace", author: "Leo Tolstoy", pages: 1392},
{title: "The Odyssey", author: "Homer", pages: 374}, {title: "The Odyssey", author: "Homer", pages: 374},
{title: "Ulysses", author: "James Joyce", pages: 730} {title: "Ulysses", author: "James Joyce", pages: 730}
]) ])`;
`;
export const findOrwellCommand = `db.sampleCollection.find({author: "George Orwell"}) export const queriesCommand = `db.sampleCollection.find({author: "George Orwell"})
`;
export const findOrwellCommandForDisplay = `// Query to find all books written by "George Orwell"
db.sampleCollection.find({author: "George Orwell"})`;
export const findByPagesCommand = `db.sampleCollection.find({pages: {$gt: 500}})
`;
export const findByPagesCommandForDisplay = `// Query to find all books with more than 500 pages
db.sampleCollection.find({pages: {$gt: 500}}) db.sampleCollection.find({pages: {$gt: 500}})
`;
export const findAndSortCommand = `db.sampleCollection.find({}).sort({pages: 1}) db.sampleCollection.find({}).sort({pages: 1})`;
`;
export const findAndSortCommandForDisplay = `// Query to find all books and sort them by the number of pages in ascending order export const queriesCommandForDisplay = `// Query to find all books written by "George Orwell"
db.sampleCollection.find({}).sort({pages: 1}) db.sampleCollection.find({author: "George Orwell"})
`;
// Query to find all books with more than 500 pages
db.sampleCollection.find({pages: {$gt: 500}})
// Query to find all books and sort them by the number of pages in ascending order
db.sampleCollection.find({}).sort({pages: 1})`;

View File

@@ -9,17 +9,15 @@ import {
Text, Text,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities"; import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities";
import { import {
findAndSortCommand,
findAndSortCommandForDisplay,
findByPagesCommand,
findByPagesCommandForDisplay,
findOrwellCommand,
findOrwellCommandForDisplay,
loadDataCommand, loadDataCommand,
newDbAndCollectionCommand, newDbAndCollectionCommand,
newDbAndCollectionCommandForDisplay, newDbAndCollectionCommandForDisplay,
queriesCommand,
queriesCommandForDisplay,
} from "Explorer/Quickstart/VCoreMongoQuickstartCommands"; } from "Explorer/Quickstart/VCoreMongoQuickstartCommands";
import { useTerminal } from "hooks/useTerminal"; import { useTerminal } from "hooks/useTerminal";
import React, { useState } from "react"; import React, { useState } from "react";
@@ -65,16 +63,28 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
> >
<Stack style={{ marginTop: 20 }}> <Stack style={{ marginTop: 20 }}>
<Text> <Text>
This tutorial guides you to create and query distributed tables using a sample dataset. A hosted mongosh (mongo shell) is provided for this quick start. You are automatically logged in to
mongosh, allowing you to interact with your database directly.
<br /> <br />
<br /> <br />
To start, input the admin password you used during the cluster creation process into the MongoDB vCore When not in the quick start guide, connecting to Azure Cosmos DB for MongoDB vCore is straightforward
terminal. using your connection string.
<br /> <br />
<br /> <br />
<Link
aria-label="View connection string"
href=""
onClick={() => sendMessage({ type: MessageTypes.OpenVCoreMongoConnectionStringsBlade })}
>
View connection string
</Link>
<br /> <br />
Note: If you navigate out of the Quick start blade &#40;MongoDB vCore Shell&#41;, the session will be <br />
closed and all ongoing commands might be interrupted. This string contains placeholders for &lt;user&gt; and &lt;password&gt;. Replace them with your chosen
username and password to establish a secure connection to your cluster. Depending on your environment,
you may need to adjust firewall rules or configure private endpoints in the &lsquo;Networking&rsquo;
tab of your database settings, or modify your own network&apos;s firewall settings, to successfully
connect.
</Text> </Text>
</Stack> </Stack>
</PivotItem> </PivotItem>
@@ -93,7 +103,6 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
documents are similar to the columns in a relational database table. One key advantage of MongoDB is documents are similar to the columns in a relational database table. One key advantage of MongoDB is
that these documents within a collection can have different fields. that these documents within a collection can have different fields.
<br /> <br />
<br />
You&apos;re now going to create a new database and a collection within that database using the Mongo You&apos;re now going to create a new database and a collection within that database using the Mongo
shell. In MongoDB, creating a database or a collection is implicit. This means that databases and shell. In MongoDB, creating a database or a collection is implicit. This means that databases and
collections are created when you first reference them in a command, so no explicit creation command is collections are created when you first reference them in a command, so no explicit creation command is
@@ -144,14 +153,14 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
with data. In MongoDB, data is stored as documents, which are structured as field and value pairs. with data. In MongoDB, data is stored as documents, which are structured as field and value pairs.
<br /> <br />
<br /> <br />
We&apos;ll add 10 documents representing books, each with a title, author, and number of pages, to Let&apos;s populate your sampleCollection with data. We&apos;ll add 10 documents representing books,
your sampleCollection in the quickstartDB database. each with a title, author, and number of pages, to your sampleCollection in the quickstartDB database.
</Text> </Text>
<DefaultButton <DefaultButton
style={{ marginTop: 16, width: 200 }} style={{ marginTop: 16, width: 200 }}
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)} onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
> >
Load data Create distributed table
</DefaultButton> </DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}> <Stack horizontal style={{ marginTop: 16 }}>
<TextField <TextField
@@ -188,23 +197,23 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
> >
<Stack style={{ marginTop: 20 }}> <Stack style={{ marginTop: 20 }}>
<Text> <Text>
Once you&apos;ve inserted data into your sampleCollection, you can retrieve it using queries. MongoDB Once youve inserted data into your sampleCollection, you can retrieve it using queries. MongoDB
queries can be as simple or as complex as you need them to be, allowing you to filter, sort, and limit queries can be as simple or as complex as you need them to be, allowing you to filter, sort, and limit
results. results.
</Text> </Text>
<DefaultButton <DefaultButton
style={{ marginTop: 16, width: 110 }} style={{ marginTop: 16, width: 110 }}
onClick={() => useTerminal.getState().sendMessage(findOrwellCommand)} onClick={() => useTerminal.getState().sendMessage(queriesCommand)}
> >
Try query Load data
</DefaultButton> </DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}> <Stack horizontal style={{ marginTop: 16 }}>
<TextField <TextField
id="findOrwellCommand" id="queriesCommand"
multiline multiline
rows={2} rows={5}
readOnly readOnly
defaultValue={findOrwellCommandForDisplay} defaultValue={queriesCommandForDisplay}
styles={{ styles={{
root: { width: "90%" }, root: { width: "90%" },
field: { field: {
@@ -218,71 +227,13 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
iconProps={{ iconProps={{
iconName: "Copy", iconName: "Copy",
}} }}
onClick={() => onCopyBtnClicked("#findOrwellCommand")} onClick={() => onCopyBtnClicked("#queriesCommand")}
/>
</Stack>
<DefaultButton
style={{ marginTop: 32, width: 110 }}
onClick={() => useTerminal.getState().sendMessage(findByPagesCommand)}
>
Try query
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}>
<TextField
id="findByPagesCommand"
multiline
rows={2}
readOnly
defaultValue={findByPagesCommandForDisplay}
styles={{
root: { width: "90%" },
field: {
backgroundColor: "#EEEEEE",
fontFamily:
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
},
}}
/>
<IconButton
iconProps={{
iconName: "Copy",
}}
onClick={() => onCopyBtnClicked("#findByPagesCommand")}
/>
</Stack>
<DefaultButton
style={{ marginTop: 32, width: 110 }}
onClick={() => useTerminal.getState().sendMessage(findAndSortCommand)}
>
Try query
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}>
<TextField
id="findAndSortCommand"
multiline
rows={2}
readOnly
defaultValue={findAndSortCommandForDisplay}
styles={{
root: { width: "90%" },
field: {
backgroundColor: "#EEEEEE",
fontFamily:
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
},
}}
/>
<IconButton
iconProps={{
iconName: "Copy",
}}
onClick={() => onCopyBtnClicked("#findAndSortCommand")}
/> />
</Stack> </Stack>
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem <PivotItem
headerText="Next steps" headerText="Integrations"
onRenderItemLink={(props, defaultRenderer) => onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4) customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
} }
@@ -291,18 +242,46 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
> >
<Stack> <Stack>
<Text> <Text>
<b>Migrate existing data</b> Cosmos DB for MongoDB vCore seamlessly integrates with Azure services. These integrations enable
<br /> Cosmos DB for MongoDB and its partner products to directly interoperate, ensuring a smooth and unified
<br /> experience, that just works.
Modernize your data seamlessly from an existing MongoDB cluster, whether it&apos;s on-premises or
hosted in the cloud, to Azure Cosmos DB for MongoDB vCore.&nbsp;
<Link
target="_blank"
href="https://learn.microsoft.com/azure/cosmos-db/mongodb/vcore/migration-options"
>
Learn more
</Link>
</Text> </Text>
<Stack horizontal>
<Stack style={{ marginTop: 20, marginRight: 20 }}>
<Text>
<b>First party integrations</b>
<br />
<br />
<b>Azure Monitor</b>
<br />
Azure monitor provides comprehensive monitoring and diagnostics for Cosmos DB for Mongo DB. Learn
more
<br />
<br />
<b>Azure Networking</b>
<br />
Azure Networking seamlessly integrates with Azure Cosmos DB for Mongo DB for fast and secure data
access. Learn more
<br />
<br />
<b>PowerShell/CLI/ARM</b>
<br />
PowerShell/CLI/ARM seamlessly integrates with Azure Cosmos DB for Mongo DB for efficient
management and automation. Learn more
</Text>
</Stack>
<Stack style={{ marginTop: 20, marginLeft: 20 }}>
<Text>
<b>Application platforms integrations</b>
<br />
<br />
<b>Vercel</b>
<br />
Vercel is a cloud platform for hosting static front ends and serverless functions, with instant
deployments, automated scaling, and Next.js integration. Learn more
</Text>
</Stack>
</Stack>
</Stack> </Stack>
</PivotItem> </PivotItem>
</Pivot> </Pivot>

View File

@@ -19,7 +19,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { useCarousel } from "hooks/useCarousel"; import { useCarousel } from "hooks/useCarousel";
import { usePostgres } from "hooks/usePostgres"; import { usePostgres } from "hooks/usePostgres";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg"; import ConnectIcon from "../../../images/Connect_color.svg";
@@ -105,12 +104,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
(state) => state.sampleDataResourceTokenCollection, (state) => state.sampleDataResourceTokenCollection,
), ),
}, },
{
dispose: useQueryCopilot.subscribe(
() => this.setState({}),
(state) => state.copilotEnabled,
),
},
); );
} }
@@ -121,9 +114,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
private getSplashScreenButtons = (): JSX.Element => { private getSplashScreenButtons = (): JSX.Element => {
if ( if (
userContext.apiType === "SQL" && useDatabases.getState().sampleDataResourceTokenCollection &&
useQueryCopilot.getState().copilotEnabled && userContext.features.enableCopilot &&
useDatabases.getState().sampleDataResourceTokenCollection userContext.apiType === "SQL"
) { ) {
return ( return (
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}> <Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>

View File

@@ -147,7 +147,7 @@ export class CassandraAPIDataClient extends TableDataClient {
let properties = "("; let properties = "(";
let values = "("; let values = "(";
for (let property in entity) { for (let property in entity) {
if (entity[property]._ === "" || entity[property]._ === undefined) { if (entity[property]._ === null) {
continue; continue;
} }
properties = properties.concat(`${property}, `); properties = properties.concat(`${property}, `);
@@ -208,9 +208,6 @@ export class CassandraAPIDataClient extends TableDataClient {
!originalDocument[property] || !originalDocument[property] ||
newEntity[property]._.toString() !== originalDocument[property]._.toString() newEntity[property]._.toString() !== originalDocument[property]._.toString()
) { ) {
if (newEntity[property]._.toString() === "" || newEntity[property]._ === undefined) {
continue;
}
let propertyQuerySegment = this.isStringType(newEntity[property].$) let propertyQuerySegment = this.isStringType(newEntity[property].$)
? `${property} = '${newEntity[property]._}',` ? `${property} = '${newEntity[property]._}',`
: `${property} = ${newEntity[property]._},`; : `${property} = ${newEntity[property]._},`;

View File

@@ -98,7 +98,7 @@
<button <button
class="filterbtnstyle queryButton" class="filterbtnstyle queryButton"
data-bind=" data-bind="
click: refreshDocumentsGrid.bind($data, true), click: refreshDocumentsGrid,
enable: applyFilterButton.enabled" enable: applyFilterButton.enabled"
aria-label="Apply filter" aria-label="Apply filter"
tabindex="0" tabindex="0"
@@ -176,7 +176,7 @@
<img <img
class="refreshcol" class="refreshcol"
src="/refresh-cosmos.svg" src="/refresh-cosmos.svg"
data-bind="click: refreshDocumentsGrid.bind($data, false)" data-bind="click: refreshDocumentsGrid"
alt="Refresh documents" alt="Refresh documents"
tabindex="0" tabindex="0"
/> />
@@ -209,10 +209,7 @@
</table> </table>
</div> </div>
<div class="loadMore"> <div class="loadMore">
<a <a role="button" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0"
role="button"
data-bind="click: loadNextPage.bind($data, false), event: { keypress: onLoadMoreKeyInput }"
tabindex="0"
>Load more</a >Load more</a
> >
</div> </div>

View File

@@ -1,15 +1,12 @@
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { format } from "react-string-format";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import NewDocumentIcon from "../../../images/NewDocument.svg";
import UploadIcon from "../../../images/Upload_16x16.svg";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import NewDocumentIcon from "../../../images/NewDocument.svg";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import UploadIcon from "../../../images/Upload_16x16.svg";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { import {
DocumentsGridMetrics, DocumentsGridMetrics,
@@ -17,15 +14,15 @@ import {
QueryCopilotSampleContainerId, QueryCopilotSampleContainerId,
QueryCopilotSampleDatabaseId, QueryCopilotSampleDatabaseId,
} from "../../Common/Constants"; } from "../../Common/Constants";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument"; import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { readDocument } from "../../Common/dataAccess/readDocument"; import { readDocument } from "../../Common/dataAccess/readDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument"; import { updateDocument } from "../../Common/dataAccess/updateDocument";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
@@ -33,7 +30,6 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../../Utils/QueryUtils"; import * as QueryUtils from "../../Utils/QueryUtils";
import { extractPartitionKeyValues } from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog"; import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@@ -83,7 +79,6 @@ export default class DocumentsTab extends TabsBase {
private _resourceTokenPartitionKey: string; private _resourceTokenPartitionKey: string;
private _isQueryCopilotSampleContainer: boolean; private _isQueryCopilotSampleContainer: boolean;
private queryAbortController: AbortController; private queryAbortController: AbortController;
private cancelQueryTimeoutID: NodeJS.Timeout;
constructor(options: ViewModels.DocumentsTabOptions) { constructor(options: ViewModels.DocumentsTabOptions) {
super(options); super(options);
@@ -331,7 +326,6 @@ export default class DocumentsTab extends TabsBase {
this.showPartitionKey = this._shouldShowPartitionKey(); this.showPartitionKey = this._shouldShowPartitionKey();
this._isQueryCopilotSampleContainer = this._isQueryCopilotSampleContainer =
this.collection?.isSampleCollection &&
this.collection?.databaseId === QueryCopilotSampleDatabaseId && this.collection?.databaseId === QueryCopilotSampleDatabaseId &&
this.collection?.id() === QueryCopilotSampleContainerId; this.collection?.id() === QueryCopilotSampleContainerId;
} }
@@ -352,22 +346,6 @@ export default class DocumentsTab extends TabsBase {
return true; return true;
} }
/**
* Query first page of documents
* Select and query first document and display content
*/
private async autoPopulateContent(applyFilterButtonPressed?: boolean) {
// reset iterator
this._documentsIterator = this.createIterator();
// load documents
await this.loadNextPage(applyFilterButtonPressed);
// Select first document and load content
if (this.documentIds().length > 0) {
this.documentIds()[0].click();
}
}
public onShowFilterClick(): Q.Promise<any> { public onShowFilterClick(): Q.Promise<any> {
this.isFilterCreated(true); this.isFilterCreated(true);
this.isFilterExpanded(true); this.isFilterExpanded(true);
@@ -397,14 +375,15 @@ export default class DocumentsTab extends TabsBase {
return true; return true;
}; };
public async refreshDocumentsGrid(applyFilterButtonPressed?: boolean): Promise<void> { public async refreshDocumentsGrid(): Promise<void> {
// clear documents grid // clear documents grid
this.documentIds([]); this.documentIds([]);
try { try {
// reset iterator // reset iterator
this._documentsIterator = this.createIterator(); this._documentsIterator = this.createIterator();
// load documents // load documents
await this.autoPopulateContent(applyFilterButtonPressed); await this.loadNextPage();
// collapse filter // collapse filter
this.appliedFilter(this.filterContent()); this.appliedFilter(this.filterContent());
this.isFilterExpanded(false); this.isFilterExpanded(false);
@@ -427,11 +406,6 @@ export default class DocumentsTab extends TabsBase {
this.queryAbortController.abort(); this.queryAbortController.abort();
} }
/**
* TODO Doesn't seem to be used: remove?
* @param clickedDocumentId
* @returns
*/
public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> { public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> {
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) { if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
return Q(); return Q();
@@ -481,7 +455,7 @@ export default class DocumentsTab extends TabsBase {
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4); const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value); this.selectedDocumentContent.setBaseline(value);
this.initialDocumentContent(value); this.initialDocumentContent(value);
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( const partitionKeyValueArray = extractPartitionKey(
savedDocument, savedDocument,
this.partitionKey as PartitionKeyDefinition, this.partitionKey as PartitionKeyDefinition,
); );
@@ -532,10 +506,7 @@ export default class DocumentsTab extends TabsBase {
const selectedDocumentId = this.selectedDocumentId(); const selectedDocumentId = this.selectedDocumentId();
const documentContent = JSON.parse(this.selectedDocumentContent()); const documentContent = JSON.parse(this.selectedDocumentContent());
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition);
documentContent,
this.partitionKey as PartitionKeyDefinition,
);
selectedDocumentId.partitionKeyValue = partitionKeyValueArray; selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
this.isExecutionError(false); this.isExecutionError(false);
@@ -653,7 +624,8 @@ export default class DocumentsTab extends TabsBase {
if (!this._documentsIterator) { if (!this._documentsIterator) {
try { try {
await this.autoPopulateContent(); this._documentsIterator = this.createIterator();
await this.loadNextPage();
} catch (error) { } catch (error) {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
@@ -744,35 +716,9 @@ export default class DocumentsTab extends TabsBase {
this.initDocumentEditor(documentId, content); this.initDocumentEditor(documentId, content);
} }
public loadNextPage(applyFilterButtonClicked?: boolean): Q.Promise<any> { public loadNextPage(): Q.Promise<any> {
this.isExecuting(true); this.isExecuting(true);
this.isExecutionError(false); this.isExecutionError(false);
let automaticallyCancelQueryAfterTimeout: boolean;
if (applyFilterButtonClicked && this.queryTimeoutEnabled()) {
const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout);
automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean(
StorageKey.AutomaticallyCancelQueryAfterTimeout,
);
const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => {
if (this.isExecuting()) {
if (automaticallyCancelQueryAfterTimeout) {
this.queryAbortController.abort();
} else {
useDialog
.getState()
.showOkCancelModalDialog(
QueryConstants.CancelQueryTitle,
format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached),
"Yes",
() => this.queryAbortController.abort(),
"No",
undefined,
);
}
}
}, queryTimeout);
this.cancelQueryTimeoutID = cancelQueryTimeoutID;
}
return this._loadNextPageInternal() return this._loadNextPageInternal()
.then( .then(
(documentsIdsResponse = []) => { (documentsIdsResponse = []) => {
@@ -828,15 +774,7 @@ export default class DocumentsTab extends TabsBase {
} }
}, },
) )
.finally(() => { .finally(() => this.isExecuting(false));
this.isExecuting(false);
if (applyFilterButtonClicked && this.queryTimeoutEnabled()) {
clearTimeout(this.cancelQueryTimeoutID);
if (!automaticallyCancelQueryAfterTimeout) {
useDialog.getState().closeDialog();
}
}
});
} }
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => { public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
@@ -1014,8 +952,4 @@ export default class DocumentsTab extends TabsBase {
useSelectedNode.getState().isQueryCopilotCollectionSelected(), useSelectedNode.getState().isQueryCopilotCollectionSelected(),
}; };
} }
private queryTimeoutEnabled(): boolean {
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
}
} }

View File

@@ -1,5 +1,4 @@
import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos"; import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
import { extractPartitionKeyValues } from "Utils/QueryUtils";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
@@ -89,7 +88,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
) )
.then( .then(
(savedDocument: any) => { (savedDocument: any) => {
const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( let partitionKeyArray = extractPartitionKey(
savedDocument, savedDocument,
this._getPartitionKeyDefinition() as PartitionKeyDefinition, this._getPartitionKeyDefinition() as PartitionKeyDefinition,
); );
@@ -151,7 +150,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
this.documentIds().forEach((documentId: DocumentId) => { this.documentIds().forEach((documentId: DocumentId) => {
if (documentId.rid === updatedDocument._rid) { if (documentId.rid === updatedDocument._rid) {
const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( const partitionKeyArray = extractPartitionKey(
updatedDocument, updatedDocument,
this._getPartitionKeyDefinition() as PartitionKeyDefinition, this._getPartitionKeyDefinition() as PartitionKeyDefinition,
); );
@@ -290,7 +289,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
} }
private _hasShardKeySpecified(document: any): boolean { private _hasShardKeySpecified(document: any): boolean {
return Boolean(extractPartitionKeyValues(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition)); return Boolean(extractPartitionKey(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition));
} }
private _getPartitionKeyDefinition(): DataModels.PartitionKey { private _getPartitionKeyDefinition(): DataModels.PartitionKey {

View File

@@ -1,16 +1,11 @@
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
import { userContext } from "UserContext";
import React from "react"; import React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import type { QueryTabOptions } from "../../../Contracts/ViewModels"; import type { QueryTabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs"; import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import QueryTabComponent, { import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent";
IQueryTabComponentProps,
ITabAccessor,
QueryTabFunctionComponent,
} from "../../Tabs/QueryTab/QueryTabComponent";
import TabsBase from "../TabsBase"; import TabsBase from "../TabsBase";
import QueryTabComponent from "./QueryTabComponent";
export interface IQueryTabProps { export interface IQueryTabProps {
container: Explorer; container: Explorer;
@@ -45,13 +40,7 @@ export class NewQueryTab extends TabsBase {
} }
public render(): JSX.Element { public render(): JSX.Element {
return userContext.apiType === "SQL" ? ( return <QueryTabComponent {...this.iQueryTabComponentProps} />;
<CopilotProvider>
<QueryTabFunctionComponent {...this.iQueryTabComponentProps} />
</CopilotProvider>
) : (
<QueryTabComponent {...this.iQueryTabComponentProps} />
);
} }
public onTabClick(): void { public onTabClick(): void {

View File

@@ -1,16 +1,6 @@
import { fireEvent, render } from "@testing-library/react"; import { fireEvent, render } from "@testing-library/react";
import { CollectionTabKind } from "Contracts/ViewModels"; import QueryTabComponent, { IQueryTabComponentProps } from "Explorer/Tabs/QueryTab/QueryTabComponent";
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import QueryTabComponent, {
IQueryTabComponentProps,
QueryTabFunctionComponent,
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
import TabsBase from "Explorer/Tabs/TabsBase";
import { updateUserContext, userContext } from "UserContext";
import { mount } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs";
import React from "react"; import React from "react";
jest.mock("Explorer/Controls/Editor/EditorReact"); jest.mock("Explorer/Controls/Editor/EditorReact");
@@ -21,15 +11,9 @@ describe("QueryTabComponent", () => {
mockStore.showCopilotSidebar = false; mockStore.showCopilotSidebar = false;
mockStore.setShowCopilotSidebar = jest.fn(); mockStore.setShowCopilotSidebar = jest.fn();
}); });
afterEach(() => jest.clearAllMocks()); beforeEach(() => jest.clearAllMocks());
it("should launch conversational Copilot when ALT+C is pressed and when copilot version is 3", () => { it("should launch Copilot when ALT+C is pressed", () => {
updateUserContext({
features: {
...userContext.features,
copilotVersion: "v3.0",
},
});
const propsMock: Readonly<IQueryTabComponentProps> = { const propsMock: Readonly<IQueryTabComponentProps> = {
collection: { databaseId: "CopilotSampleDb" }, collection: { databaseId: "CopilotSampleDb" },
onTabAccessor: () => jest.fn(), onTabAccessor: () => jest.fn(),
@@ -47,32 +31,4 @@ describe("QueryTabComponent", () => {
expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true); expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true);
}); });
it("copilot should be enabled by default when tab is active", () => {
useQueryCopilot.getState().setCopilotEnabled(true);
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
const activeTab = new TabsBase({
tabKind: CollectionTabKind.Query,
title: "Query",
tabPath: "",
});
activeTab.tabId = "mockTabId";
useTabs.getState().activeTab = activeTab;
const propsMock: Readonly<IQueryTabComponentProps> = {
collection: { databaseId: "CopilotUserDb", id: () => "CopilotUserContainer" },
onTabAccessor: () => jest.fn(),
isExecutionError: false,
tabId: "mockTabId",
tabsBaseInstance: {
updateNavbarWithTabsButtons: () => jest.fn(),
},
} as unknown as IQueryTabComponentProps;
const container = mount(
<CopilotProvider>
<QueryTabFunctionComponent {...propsMock} />
</CopilotProvider>,
);
expect(container.find(QueryCopilotPromptbar).exists()).toBe(true);
});
}); });

View File

@@ -1,29 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { useDialog } from "Explorer/Controls/Dialog"; import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { TabsState, useTabs } from "hooks/useTabs";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import SplitterLayout from "react-splitter-layout"; import SplitterLayout from "react-splitter-layout";
import "react-splitter-layout/lib/index.css"; import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format";
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import SaveQueryIcon from "../../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../../images/save-cosmos.svg";
import { NormalizedEventKey } from "../../../Common/Constants"; import { NormalizedEventKey, QueryCopilotSampleDatabaseId } from "../../../Common/Constants";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../../Common/HeadersUtility"; import * as HeadersUtility from "../../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
@@ -32,8 +20,6 @@ import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import * as StringUtility from "../../../Shared/StringUtility";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as QueryUtils from "../../../Utils/QueryUtils"; import * as QueryUtils from "../../../Utils/QueryUtils";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
@@ -82,9 +68,6 @@ export interface IQueryTabComponentProps {
isPreferredApiMongoDB?: boolean; isPreferredApiMongoDB?: boolean;
monacoEditorSetting?: string; monacoEditorSetting?: string;
viewModelcollection?: ViewModels.Collection; viewModelcollection?: ViewModels.Collection;
copilotEnabled?: boolean;
isSampleCopilotActive?: boolean;
copilotStore?: Partial<QueryCopilotState>;
} }
interface IQueryTabStates { interface IQueryTabStates {
@@ -97,26 +80,8 @@ interface IQueryTabStates {
isExecuting: boolean; isExecuting: boolean;
showCopilotSidebar: boolean; showCopilotSidebar: boolean;
queryCopilotGeneratedQuery: string; queryCopilotGeneratedQuery: string;
cancelQueryTimeoutID: NodeJS.Timeout;
copilotActive: boolean;
currentTabActive: boolean;
} }
export const QueryTabFunctionComponent = (props: IQueryTabComponentProps): any => {
const copilotStore = useCopilotStore();
const copilotGlobalStore = useQueryCopilot();
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
const queryTabProps = {
...props,
copilotEnabled:
copilotGlobalStore.copilotEnabled &&
(copilotGlobalStore.copilotUserDBEnabled || (isSampleCopilotActive && !!userContext.sampleDataConnectionInfo)),
isSampleCopilotActive: isSampleCopilotActive,
copilotStore: copilotStore,
};
return <QueryTabComponent {...queryTabProps}></QueryTabComponent>;
};
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> { export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
public queryEditorId: string; public queryEditorId: string;
public executeQueryButton: Button; public executeQueryButton: Button;
@@ -142,15 +107,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
isExecuting: false, isExecuting: false,
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar, showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
queryCopilotGeneratedQuery: useQueryCopilot.getState().query, queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
cancelQueryTimeoutID: undefined,
copilotActive: this._queryCopilotActive(),
currentTabActive: true,
}; };
this.isCloseClicked = false; this.isCloseClicked = false;
this.splitterId = this.props.tabId + "_splitter"; this.splitterId = this.props.tabId + "_splitter";
this.queryEditorId = `queryeditor${this.props.tabId}`; this.queryEditorId = `queryeditor${this.props.tabId}`;
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB; this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
this.isCopilotTabActive = userContext.features.copilotVersion === "v3.0"; this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
this.executeQueryButton = { this.executeQueryButton = {
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0, enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
visible: true, visible: true,
@@ -175,19 +138,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}); });
} }
private _queryCopilotActive(): boolean {
if (this.props.copilotEnabled) {
const cachedCopilotToggleStatus: string = localStorage.getItem(
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
);
const copilotInitialActive: boolean = cachedCopilotToggleStatus
? StringUtility.toBoolean(cachedCopilotToggleStatus)
: true;
return copilotInitialActive;
}
return false;
}
public onCloseClick(isClicked: boolean): void { public onCloseClick(isClicked: boolean): void {
this.isCloseClicked = isClicked; this.isCloseClicked = isClicked;
if (useQueryCopilot.getState().wasCopilotUsed && this.isCopilotTabActive) { if (useQueryCopilot.getState().wasCopilotUsed && this.isCopilotTabActive) {
@@ -212,16 +162,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
setTimeout(async () => { setTimeout(async () => {
await this._executeQueryDocumentsPage(0); await this._executeQueryDocumentsPage(0);
}, 100); }, 100);
if (this.state.copilotActive) {
const query = this.state.sqlQueryEditorContent.split("\r\n")?.pop();
const isqueryEdited = this.props.copilotStore.generatedQuery && this.props.copilotStore.generatedQuery !== query;
if (isqueryEdited) {
TelemetryProcessor.traceMark(Action.QueryEdited, {
databaseName: this.props.collection.databaseId,
collectionId: this.props.collection.id(),
});
}
}
}; };
public onSaveQueryClick = (): void => { public onSaveQueryClick = (): void => {
@@ -310,34 +250,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this.setState({ this.setState({
isExecuting: true, isExecuting: true,
}); });
let automaticallyCancelQueryAfterTimeout: boolean;
if (this.queryTimeoutEnabled()) {
const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout);
automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean(
StorageKey.AutomaticallyCancelQueryAfterTimeout,
);
const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => {
if (this.state.isExecuting) {
if (automaticallyCancelQueryAfterTimeout) {
this.queryAbortController.abort();
} else {
useDialog
.getState()
.showOkCancelModalDialog(
QueryConstants.CancelQueryTitle,
format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached),
"Yes",
() => this.queryAbortController.abort(),
"No",
undefined,
);
}
}
}, queryTimeout);
this.setState({
cancelQueryTimeoutID,
});
}
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
try { try {
@@ -361,14 +273,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this.props.tabsBaseInstance.isExecuting(false); this.props.tabsBaseInstance.isExecuting(false);
this.setState({ this.setState({
isExecuting: false, isExecuting: false,
cancelQueryTimeoutID: undefined,
}); });
if (this.queryTimeoutEnabled()) {
clearTimeout(this.state.cancelQueryTimeoutID);
if (!automaticallyCancelQueryAfterTimeout) {
useDialog.getState().closeDialog();
}
}
this.togglesOnFocus(); this.togglesOnFocus();
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
@@ -381,9 +286,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({ buttons.push({
iconSrc: ExecuteQueryIcon, iconSrc: ExecuteQueryIcon,
iconAlt: label, iconAlt: label,
onCommandClick: this.props.isSampleCopilotActive onCommandClick: this.isCopilotTabActive ? () => OnExecuteQueryClick() : this.onExecuteQueryClick,
? () => OnExecuteQueryClick(this.props.copilotStore)
: this.onExecuteQueryClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: false, hasPopup: false,
@@ -437,20 +340,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push(launchCopilotButton); buttons.push(launchCopilotButton);
} }
if (this.props.copilotEnabled) {
const toggleCopilotButton = {
iconSrc: QueryCommandIcon,
iconAlt: "Copilot",
onCommandClick: () => {
this._toggleCopilot(!this.state.copilotActive);
},
commandButtonLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot",
ariaLabel: "Copilot",
hasPopup: false,
};
buttons.push(toggleCopilotButton);
}
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) { if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
const label = "Cancel query"; const label = "Cancel query";
buttons.push({ buttons.push({
@@ -466,31 +355,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
return buttons; return buttons;
} }
private _toggleCopilot = (active: boolean) => {
this.setState({ copilotActive: active });
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, active.toString());
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
databaseName: this.props.collection.databaseId,
collectionId: this.props.collection.id(),
});
};
componentDidUpdate = (_prevProps: IQueryTabComponentProps, prevState: IQueryTabStates): void => {
if (prevState.copilotActive !== this.state.copilotActive) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
};
public onChangeContent(newContent: string): void { public onChangeContent(newContent: string): void {
this.setState({ this.setState({
sqlQueryEditorContent: newContent, sqlQueryEditorContent: newContent,
queryCopilotGeneratedQuery: "", queryCopilotGeneratedQuery: "",
}); });
if (this.state.copilotActive) {
this.props.copilotStore?.setQuery(newContent);
}
if (this.isPreferredApiMongoDB) { if (this.isPreferredApiMongoDB) {
if (newContent.length > 0) { if (newContent.length > 0) {
this.executeQueryButton = { this.executeQueryButton = {
@@ -525,10 +394,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
: useQueryCopilot.getState().setSelectedQuery(""); : useQueryCopilot.getState().setSelectedQuery("");
} }
if (this.state.copilotActive) {
this.props.copilotStore?.setSelectedQuery(selectedContent);
}
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
@@ -537,29 +402,18 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
return this.state.queryCopilotGeneratedQuery; return this.state.queryCopilotGeneratedQuery;
} }
if (this.state.copilotActive) {
return this.props.copilotStore?.query;
}
return this.state.sqlQueryEditorContent; return this.state.sqlQueryEditorContent;
} }
private queryTimeoutEnabled(): boolean {
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
}
private unsubscribeCopilotSidebar: () => void; private unsubscribeCopilotSidebar: () => void;
componentDidMount(): void { componentDidMount(): void {
useTabs.subscribe((state: TabsState) => { this.unsubscribeCopilotSidebar = useQueryCopilot.subscribe((state: QueryCopilotState) => {
if (this.state.currentTabActive && state.activeTab?.tabId !== this.props.tabId) { if (this.state.showCopilotSidebar !== state.showCopilotSidebar) {
this.setState({ this.setState({ showCopilotSidebar: state.showCopilotSidebar });
currentTabActive: false, }
}); if (this.state.queryCopilotGeneratedQuery !== state.query) {
} else if (!this.state.currentTabActive && state.activeTab?.tabId === this.props.tabId) { this.setState({ queryCopilotGeneratedQuery: state.query });
this.setState({
currentTabActive: true,
});
} }
}); });
@@ -568,6 +422,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
componentWillUnmount(): void { componentWillUnmount(): void {
this.unsubscribeCopilotSidebar();
document.removeEventListener("keydown", this.handleCopilotKeyDown); document.removeEventListener("keydown", this.handleCopilotKeyDown);
} }
@@ -575,14 +430,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
return ( return (
<Fragment> <Fragment>
<div className="tab-pane" id={this.props.tabId} role="tabpanel"> <div className="tab-pane" id={this.props.tabId} role="tabpanel">
{this.props.copilotEnabled && this.state.currentTabActive && this.state.copilotActive && (
<QueryCopilotPromptbar
explorer={this.props.collection.container}
toggleCopilot={this._toggleCopilot}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
></QueryCopilotPromptbar>
)}
<div className="tabPaneContentContainer"> <div className="tabPaneContentContainer">
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}> <SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
<Fragment> <Fragment>
@@ -591,7 +438,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
language={"sql"} language={"sql"}
content={this.setEditorContent()} content={this.setEditorContent()}
isReadOnly={false} isReadOnly={false}
wordWrap={"on"}
ariaLabel={"Editing Query"} ariaLabel={"Editing Query"}
lineNumbers={"on"} lineNumbers={"on"}
onContentChanged={(newContent: string) => this.onChangeContent(newContent)} onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
@@ -599,21 +445,8 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
/> />
</div> </div>
</Fragment> </Fragment>
{this.props.isSampleCopilotActive ? ( {this.isCopilotTabActive ? (
<QueryResultSection <QueryCopilotResults />
isMongoDB={this.props.isPreferredApiMongoDB}
queryEditorContent={this.state.sqlQueryEditorContent}
error={this.props.copilotStore?.errorMessage}
queryResults={this.props.copilotStore?.queryResults}
isExecuting={this.props.copilotStore?.isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage(
firstItemIndex,
this.props.copilotStore.queryIterator,
this.props.copilotStore,
)
}
/>
) : ( ) : (
<QueryResultSection <QueryResultSection
isMongoDB={this.props.isPreferredApiMongoDB} isMongoDB={this.props.isPreferredApiMongoDB}
@@ -629,14 +462,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
</SplitterLayout> </SplitterLayout>
</div> </div>
</div> </div>
{this.props.copilotEnabled && this.props.copilotStore?.showFeedbackModal && (
<QueryCopilotFeedbackModal
explorer={this.props.collection.container}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
mode={this.props.isSampleCopilotActive ? "Sample" : "User"}
/>
)}
</Fragment> </Fragment>
); );
} }

View File

@@ -11,6 +11,7 @@ import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout"; import ko from "knockout";
import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import React, { MutableRefObject, useEffect, useRef, useState } from "react";
@@ -169,7 +170,7 @@ const CloseButton = ({
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
event.stopPropagation(); event.stopPropagation();
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind); tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
// tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates(); tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates();
}} }}
tabIndex={active ? 0 : undefined} tabIndex={active ? 0 : undefined}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)} onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
@@ -255,7 +256,6 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
}; };
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
// eslint-disable-next-line no-console
switch (activeReactTab) { switch (activeReactTab) {
case ReactTabKind.Connect: case ReactTabKind.Connect:
return userContext.apiType === "VCoreMongo" ? ( return userContext.apiType === "VCoreMongo" ? (

View File

@@ -3,15 +3,15 @@ import * as Constants from "../../Common/Constants";
import * as ThemeUtility from "../../Common/ThemeUtility"; import * as ThemeUtility from "../../Common/ThemeUtility";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useNotificationConsole } from "../../hooks/useNotificationConsole"; import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
// TODO: Use specific actions for logging telemetry data // TODO: Use specific actions for logging telemetry data
export default class TabsBase extends WaitsForTemplateViewModel { export default class TabsBase extends WaitsForTemplateViewModel {
private static id = 0; private static id = 0;

View File

@@ -177,8 +177,8 @@ export default class Collection implements ViewModels.Collection {
this.children.subscribe(() => { this.children.subscribe(() => {
// update the database in zustand store // update the database in zustand store
const database = this.getDatabase(); const database = this.getDatabase();
database?.collections( database.collections(
database?.collections()?.map((collection) => { database.collections()?.map((collection) => {
if (collection.id() === this.id()) { if (collection.id() === this.id()) {
return this; return this;
} }

View File

@@ -1,4 +1,4 @@
import { extractPartitionKeyValues } from "Utils/QueryUtils"; import { extractPartitionKey } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { readDocument } from "../../Common/dataAccess/readDocument"; import { readDocument } from "../../Common/dataAccess/readDocument";
@@ -42,7 +42,7 @@ export default class ConflictId {
} }
this.partitionKeyProperty = container && container.partitionKeyProperty; this.partitionKeyProperty = container && container.partitionKeyProperty;
this.partitionKey = container && container.partitionKey; this.partitionKey = container && container.partitionKey;
this.partitionKeyValue = extractPartitionKeyValues(this.parsedContent, this.partitionKey as any); this.partitionKeyValue = extractPartitionKey(this.parsedContent, this.partitionKey as any);
this.stringPartitionKeyValue = this.getPartitionKeyValueAsString(); this.stringPartitionKeyValue = this.getPartitionKeyValueAsString();
this.id = ko.observable(data.id); this.id = ko.observable(data.id);
this.isDirty = ko.observable(false); this.isDirty = ko.observable(false);

View File

@@ -156,6 +156,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
} }
public getDatabase(): ViewModels.Database { public getDatabase(): ViewModels.Database {
return useDatabases.getState().findDatabaseWithId(this.databaseId, this.isSampleCollection); return useDatabases.getState().findDatabaseWithId(this.databaseId);
} }
} }

View File

@@ -1,7 +1,6 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
import { getItemName } from "Utils/APITypeUtils"; import { getItemName } from "Utils/APITypeUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as React from "react"; import * as React from "react";
import shallow from "zustand/shallow"; import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
@@ -768,8 +767,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}; };
const dataRootNode = buildDataTree(); const dataRootNode = buildDataTree();
const isSampleDataEnabled = const isSampleDataEnabled = userContext.sampleDataConnectionInfo && userContext.apiType === "SQL";
useQueryCopilot().copilotEnabled && userContext.sampleDataConnectionInfo && userContext.apiType === "SQL";
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection); const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
return ( return (

View File

@@ -5,6 +5,7 @@ import { useDatabases } from "Explorer/useDatabases";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -21,7 +22,7 @@ export const useDatabaseTreeNodes = (container: Explorer, isNotebookEnabled: boo
className: "databaseHeader", className: "databaseHeader",
children: [], children: [],
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()), isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
contextMenu: undefined, // TODO Disable this for now as the actions don't work. ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()), contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
onExpanded: async () => { onExpanded: async () => {
useSelectedNode.getState().setSelectedNode(database); useSelectedNode.getState().setSelectedNode(database);
if (!databaseNode.children || databaseNode.children?.length === 0) { if (!databaseNode.children || databaseNode.children?.length === 0) {

View File

@@ -14,7 +14,7 @@ interface DatabasesState {
deleteDatabase: (database: ViewModels.Database) => void; deleteDatabase: (database: ViewModels.Database) => void;
clearDatabases: () => void; clearDatabases: () => void;
isSaveQueryEnabled: () => boolean; isSaveQueryEnabled: () => boolean;
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => ViewModels.Database; findDatabaseWithId: (databaseId: string) => ViewModels.Database;
isLastNonEmptyDatabase: () => boolean; isLastNonEmptyDatabase: () => boolean;
findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection; findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection;
isLastCollection: () => boolean; isLastCollection: () => boolean;
@@ -33,7 +33,7 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
updateDatabase: (updatedDatabase: ViewModels.Database) => updateDatabase: (updatedDatabase: ViewModels.Database) =>
set((state) => { set((state) => {
const updatedDatabases = state.databases.map((database: ViewModels.Database) => { const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
if (database?.id() === updatedDatabase?.id()) { if (database.id() === updatedDatabase.id()) {
return updatedDatabase; return updatedDatabase;
} }
@@ -67,9 +67,7 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
} }
return true; return true;
}, },
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => { findDatabaseWithId: (databaseId: string) => get().databases.find((db) => databaseId === db.id()),
return get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase);
},
isLastNonEmptyDatabase: () => { isLastNonEmptyDatabase: () => {
const databases = get().databases; const databases = get().databases;
return databases.length === 1 && (databases[0].collections()?.length > 0 || !!databases[0].offer()); return databases.length === 1 && (databases[0].collections()?.length > 0 || !!databases[0].offer());

View File

@@ -7,6 +7,9 @@ import "../less/hostedexplorer.less";
import { AuthType } from "./AuthType"; import { AuthType } from "./AuthType";
import { DatabaseAccount } from "./Contracts/DataModels"; import { DatabaseAccount } from "./Contracts/DataModels";
import "./Explorer/Menus/NavBar/MeControlComponent.less"; import "./Explorer/Menus/NavBar/MeControlComponent.less";
import { useAADAuth } from "./hooks/useAADAuth";
import { useConfig } from "./hooks/useConfig";
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher"; import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer"; import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
@@ -17,9 +20,6 @@ import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
import "./Platform/Hosted/ConnectScreen.less"; import "./Platform/Hosted/ConnectScreen.less";
import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils"; import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils";
import "./Shared/appInsights"; import "./Shared/appInsights";
import { useAADAuth } from "./hooks/useAADAuth";
import { useConfig } from "./hooks/useConfig";
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
initializeIcons(); initializeIcons();

View File

@@ -16,8 +16,9 @@ import "../externals/jquery.dataTables.min.css";
import "../externals/jquery.typeahead.min.css"; import "../externals/jquery.typeahead.min.css";
import "../externals/jquery.typeahead.min.js"; import "../externals/jquery.typeahead.min.js";
// Image Dependencies // Image Dependencies
import { Platform } from "ConfigContext";
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel"; import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import "../images/favicon.ico"; import "../images/favicon.ico";
@@ -25,6 +26,8 @@ import "../less/TableStyles/CustomizeColumns.less";
import "../less/TableStyles/EntityEditor.less"; import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less"; import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less"; import "../less/TableStyles/queryBuilder.less";
import * as StyleConstants from "./Common/StyleConstants";
import { configContext, Platform } from "ConfigContext";
import "../less/documentDB.less"; import "../less/documentDB.less";
import "../less/forms.less"; import "../less/forms.less";
import "../less/infobox.less"; import "../less/infobox.less";
@@ -34,7 +37,6 @@ import "../less/resourceTree.less";
import "../less/tree.less"; import "../less/tree.less";
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree"; import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer"; import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
import * as StyleConstants from "./Common/StyleConstants";
import "./Explorer/Controls/Accordion/AccordionComponent.less"; import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import { Dialog } from "./Explorer/Controls/Dialog"; import { Dialog } from "./Explorer/Controls/Dialog";
@@ -54,10 +56,10 @@ import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
import "./Explorer/SplashScreen/SplashScreen.less"; import "./Explorer/SplashScreen/SplashScreen.less";
import { Tabs } from "./Explorer/Tabs/Tabs"; import { Tabs } from "./Explorer/Tabs/Tabs";
import "./Libs/jquery"; import "./Libs/jquery";
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
import "./Shared/appInsights"; import "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
initializeIcons(); initializeIcons();
@@ -65,11 +67,11 @@ const App: React.FunctionComponent = () => {
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true); const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
const isCarouselOpen = useCarousel((state) => state.shouldOpen); const isCarouselOpen = useCarousel((state) => state.shouldOpen);
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel); const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
const shouldShowModal = useQueryCopilot((state) => state.showFeedbackModal);
const config = useConfig(); const config = useConfig();
if (config?.platform === Platform.Fabric) { if (config?.platform === Platform.Fabric) {
loadTheme(appThemeFabric); loadTheme(appThemeFabric);
import("../less/documentDBFabric.less");
} }
StyleConstants.updateStyles(); StyleConstants.updateStyles();
const explorer = useKnockoutExplorer(config?.platform); const explorer = useKnockoutExplorer(config?.platform);
@@ -89,6 +91,7 @@ const App: React.FunctionComponent = () => {
return ( return (
<div className="flexContainer" aria-hidden="false"> <div className="flexContainer" aria-hidden="false">
<LoadFabricOverrides />
<div id="divExplorer" className="flexContainer hideOverflows"> <div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div> <div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */} {/* Main Command Bar - Start */}
@@ -133,12 +136,25 @@ const App: React.FunctionComponent = () => {
{<SQLQuickstartTutorial />} {<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />} {<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />} {<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
{shouldShowModal && <QueryCopilotFeedbackModal explorer={explorer} />}
</div> </div>
); );
}; };
const mainElement = document.getElementById("Main"); ReactDOM.render(<App />, document.body);
ReactDOM.render(<App />, mainElement);
function LoadFabricOverrides(): JSX.Element {
if (configContext.platform === Platform.Fabric) {
const FabricStyle = React.lazy(() => import("./Platform/Fabric/FabricPlatform"));
return (
<React.Suspense fallback={<div></div>}>
<FabricStyle />
</React.Suspense>
);
} else {
return <></>;
}
}
function LoadingExplorer(): JSX.Element { function LoadingExplorer(): JSX.Element {
return ( return (

View File

@@ -0,0 +1,7 @@
import React from "react";
import "../../../less/documentDBFabric.less";
// This is a dummy export, allowing us to conditionally import documentDBFabric.less
// by lazy-importing this in Main.tsx (see LoadFabricOverrides() there)
export default function InitFabric() {
return <></>;
}

View File

@@ -1,53 +0,0 @@
import { sendCachedDataMessage } from "Common/MessageHandler";
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
import { MessageTypes } from "Contracts/MessageTypes";
import Explorer from "Explorer/Explorer";
import { updateUserContext } from "UserContext";
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
let timeoutId: NodeJS.Timeout;
// Prevents multiple parallel requests
let isRequestPending = false;
export const requestDatabaseResourceTokens = (): void => {
if (isRequestPending) {
return;
}
// TODO Make Fabric return the message id so we can handle this promise
isRequestPending = true;
sendCachedDataMessage<FabricDatabaseConnectionInfo>(MessageTypes.GetAllResourceTokens, []);
};
export const handleRequestDatabaseResourceTokensResponse = (
explorer: Explorer,
fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo,
): void => {
isRequestPending = false;
updateUserContext({ fabricDatabaseConnectionInfo });
scheduleRefreshDatabaseResourceToken();
explorer.refreshAllDatabases();
};
/**
* Check token validity and schedule a refresh if necessary
* @param tokenTimestamp
* @returns
*/
export const scheduleRefreshDatabaseResourceToken = (): void => {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
timeoutId = setTimeout(() => {
requestDatabaseResourceTokens();
}, TOKEN_VALIDITY_MS);
};
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
requestDatabaseResourceTokens();
}
};

View File

@@ -1,8 +1,6 @@
jest.mock("../../../hooks/useDirectories"); jest.mock("../../../hooks/useDirectories");
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { extractFeatures } from "Platform/Hosted/extractFeatures";
import { updateUserContext, userContext } from "UserContext";
import React from "react"; import React from "react";
import { ConnectExplorer } from "./ConnectExplorer"; import { ConnectExplorer } from "./ConnectExplorer";
@@ -18,24 +16,3 @@ it("shows the connect form", () => {
fireEvent.click(screen.getByText("Connect to your account with connection string")); fireEvent.click(screen.getByText("Connect to your account with connection string"));
expect(screen.queryByPlaceholderText("Please enter a connection string")).toBeDefined(); expect(screen.queryByPlaceholderText("Please enter a connection string")).toBeDefined();
}); });
it("hides the connection string link when feature.disableConnectionStringLogin is true", () => {
const connectionString = "fakeConnectionString";
const login = jest.fn();
const setConnectionString = jest.fn();
const setEncryptedToken = jest.fn();
const setAuthType = jest.fn();
const oldFeatures = userContext.features;
const params = new URLSearchParams({
"feature.disableConnectionStringLogin": "true",
});
const testFeatures = extractFeatures(params);
updateUserContext({ features: testFeatures });
render(<ConnectExplorer {...{ login, setEncryptedToken, setAuthType, connectionString, setConnectionString }} />);
expect(screen.queryByPlaceholderText("Connect to your account with connection string")).toBeNull();
updateUserContext({ features: oldFeatures });
});

View File

@@ -1,5 +1,4 @@
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { userContext } from "UserContext";
import * as React from "react"; import * as React from "react";
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
import ErrorImage from "../../../../images/error.svg"; import ErrorImage from "../../../../images/error.svg";
@@ -30,18 +29,6 @@ export const fetchEncryptedToken = async (connectionString: string): Promise<str
return decodeURIComponent(result.readWrite || result.read); return decodeURIComponent(result.readWrite || result.read);
}; };
export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise<boolean> => {
const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) {
throw response;
}
return (await response.text()) === "True";
};
export const ConnectExplorer: React.FunctionComponent<Props> = ({ export const ConnectExplorer: React.FunctionComponent<Props> = ({
setEncryptedToken, setEncryptedToken,
login, login,
@@ -50,8 +37,6 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
setConnectionString, setConnectionString,
}: Props) => { }: Props) => {
const [isFormVisible, { setTrue: showForm }] = useBoolean(false); const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
const [errorMessage, setErrorMessage] = React.useState("");
const enableConnectionStringLogin = !userContext.features.disableConnectionStringLogin;
return ( return (
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}> <div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
@@ -61,19 +46,11 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
<img src={ConnectImage} alt="Azure Cosmos DB" /> <img src={ConnectImage} alt="Azure Cosmos DB" />
</p> </p>
<p className="welcomeText">Welcome to Azure Cosmos DB</p> <p className="welcomeText">Welcome to Azure Cosmos DB</p>
{isFormVisible && enableConnectionStringLogin ? ( {isFormVisible ? (
<form <form
id="connectWithConnectionString" id="connectWithConnectionString"
onSubmit={async (event) => { onSubmit={async (event) => {
event.preventDefault(); event.preventDefault();
setErrorMessage("");
if (await isAccountRestrictedForConnectionStringLogin(connectionString)) {
setErrorMessage(
"This account has been blocked from connection-string login. Please go to cosmos.azure.com/aad for AAD based login.",
);
return;
}
if (isResourceTokenConnectionString(connectionString)) { if (isResourceTokenConnectionString(connectionString)) {
setAuthType(AuthType.ResourceToken); setAuthType(AuthType.ResourceToken);
@@ -97,12 +74,10 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
setConnectionString(event.target.value); setConnectionString(event.target.value);
}} }}
/> />
{errorMessage.length > 0 && ( <span className="errorDetailsInfoTooltip" style={{ display: "none" }}>
<span className="errorDetailsInfoTooltip"> <img className="errorImg" src={ErrorImage} alt="Error notification" />
<img className="errorImg" src={ErrorImage} alt="Error notification" /> <span className="errorDetails"></span>
<span className="errorDetails">{errorMessage}</span> </span>
</span>
)}
</p> </p>
<p className="connectExplorerContent"> <p className="connectExplorerContent">
<input className="filterbtnstyle" type="submit" value="Connect" /> <input className="filterbtnstyle" type="submit" value="Connect" />
@@ -114,11 +89,9 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
) : ( ) : (
<div id="connectWithAad"> <div id="connectWithAad">
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} /> <input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
{enableConnectionStringLogin && ( <p className="switchConnectTypeText" onClick={showForm}>
<p className="switchConnectTypeText" onClick={showForm}> Connect to your account with connection string
Connect to your account with connection string </p>
</p>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -66,7 +66,7 @@
} }
.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails { .connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails {
bottom: 24px; bottom: 24px;
width: 165px; width: 145px;
visibility: hidden; visibility: hidden;
background-color: #393939; background-color: #393939;
color: #ffffff; color: #ffffff;

View File

@@ -14,7 +14,6 @@ export type Features = {
readonly enableTtl: boolean; readonly enableTtl: boolean;
readonly executeSproc: boolean; readonly executeSproc: boolean;
readonly enableAadDataPlane: boolean; readonly enableAadDataPlane: boolean;
readonly enableResourceGraph: boolean;
readonly enableKoResourceTree: boolean; readonly enableKoResourceTree: boolean;
readonly hostedDataExplorer: boolean; readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string; readonly junoEndpoint?: string;
@@ -42,7 +41,6 @@ export type Features = {
readonly enableCopilotFullSchema: boolean; readonly enableCopilotFullSchema: boolean;
readonly copilotChatFixedMonacoEditorHeight: boolean; readonly copilotChatFixedMonacoEditorHeight: boolean;
readonly enablePriorityBasedExecution: boolean; readonly enablePriorityBasedExecution: boolean;
readonly disableConnectionStringLogin: boolean;
// can be set via both flight and feature flag // can be set via both flight and feature flag
autoscaleDefault: boolean; autoscaleDefault: boolean;
@@ -74,7 +72,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
canExceedMaximumValue: "true" === get("canexceedmaximumvalue"), canExceedMaximumValue: "true" === get("canexceedmaximumvalue"),
cosmosdb: "true" === get("cosmosdb"), cosmosdb: "true" === get("cosmosdb"),
enableAadDataPlane: "true" === get("enableaaddataplane"), enableAadDataPlane: "true" === get("enableaaddataplane"),
enableResourceGraph: "true" === get("enableresourcegraph"),
enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"), enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"),
enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"), enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"),
enableKOPanel: "true" === get("enablekopanel"), enableKOPanel: "true" === get("enablekopanel"),
@@ -112,12 +109,11 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"), enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"), loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
enableCopilot: "true" === get("enablecopilot", "true"), enableCopilot: "true" === get("enablecopilot", "true"),
copilotVersion: get("copilotversion") ?? "v2.0", copilotVersion: get("copilotversion") ?? "v1.0",
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"), disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"), enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"), copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"), enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
}; };
} }

View File

@@ -208,9 +208,3 @@ export class FreeTierLimits {
public static RU: number = 1000; public static RU: number = 1000;
public static Storage: number = 25; public static Storage: number = 25;
} }
export class QueryConstants {
public static readonly CancelQueryTitle: string = "Cancel query";
public static readonly CancelQuerySubTextTemplate: string = "{0} Do you want to cancel this query?";
public static readonly CancelQueryTimeoutThresholdReached: string = "The query timeout threshold has been reached.";
}

View File

@@ -4,9 +4,6 @@ import * as SessionStorageUtility from "./SessionStorageUtility";
export { LocalStorageUtility, SessionStorageUtility }; export { LocalStorageUtility, SessionStorageUtility };
export enum StorageKey { export enum StorageKey {
ActualItemPerPage, ActualItemPerPage,
QueryTimeoutEnabled,
QueryTimeout,
AutomaticallyCancelQueryAfterTimeout,
ContainerPaginationEnabled, ContainerPaginationEnabled,
CustomItemPerPage, CustomItemPerPage,
DatabaseAccountId, DatabaseAccountId,

View File

@@ -133,10 +133,6 @@ export enum Action {
CompleteUITour, CompleteUITour,
OpenQueryCopilotFromSplashScreen, OpenQueryCopilotFromSplashScreen,
OpenQueryCopilotFromNewQuery, OpenQueryCopilotFromNewQuery,
ActivateQueryCopilot,
DeactivateQueryCopilot,
QueryGenerationFromCopilotPrompt,
QueryEdited,
ExecuteQueryGeneratedFromQueryCopilot, ExecuteQueryGeneratedFromQueryCopilot,
} }

View File

@@ -10,13 +10,9 @@ import { userContext } from "UserContext";
export class JupyterLabAppFactory { export class JupyterLabAppFactory {
private isShellStarted: boolean | undefined; private isShellStarted: boolean | undefined;
private checkShellStarted: ((content: string | undefined) => void) | undefined; private checkShellStarted: ((content: string | undefined) => void) | undefined;
private onShellExited: (restartShell: boolean) => void; private onShellExited: () => void;
private restartShell: boolean;
private isShellExited(content: string | undefined) { private isShellExited(content: string | undefined) {
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
this.restartShell = true;
}
return content?.includes("cosmosuser@"); return content?.includes("cosmosuser@");
} }
@@ -36,11 +32,10 @@ export class JupyterLabAppFactory {
this.isShellStarted = content?.includes("Enter password"); this.isShellStarted = content?.includes("Enter password");
} }
constructor(closeTab: (restartShell: boolean) => void) { constructor(closeTab: () => void) {
this.onShellExited = closeTab; this.onShellExited = closeTab;
this.isShellStarted = false; this.isShellStarted = false;
this.checkShellStarted = undefined; this.checkShellStarted = undefined;
this.restartShell = false;
switch (userContext.apiType) { switch (userContext.apiType) {
case "Mongo": case "Mongo":
@@ -74,7 +69,7 @@ export class JupyterLabAppFactory {
if (!this.isShellStarted) { if (!this.isShellStarted) {
this.checkShellStarted(content); this.checkShellStarted(content);
} else if (this.isShellExited(content)) { } else if (this.isShellExited(content)) {
this.onShellExited(this.restartShell); this.onShellExited();
} }
} }
}, this); }, this);

View File

@@ -11,8 +11,6 @@ import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
import { TerminalProps } from "./TerminalProps"; import { TerminalProps } from "./TerminalProps";
import "./index.css"; import "./index.css";
let session: ITerminalConnection | undefined;
const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => { const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => {
let body: BodyInit | undefined; let body: BodyInit | undefined;
let headers: HeadersInit | undefined; let headers: HeadersInit | undefined;
@@ -51,7 +49,7 @@ const createServerSettings = (props: TerminalProps): ServerConnection.ISettings
return ServerConnection.makeSettings(options); return ServerConnection.makeSettings(options);
}; };
const initTerminal = async (props: TerminalProps): Promise<void> => { const initTerminal = async (props: TerminalProps): Promise<ITerminalConnection | undefined> => {
// Initialize userContext (only properties which are needed by TelemetryProcessor) // Initialize userContext (only properties which are needed by TelemetryProcessor)
updateUserContext({ updateUserContext({
subscriptionId: props.subscriptionId, subscriptionId: props.subscriptionId,
@@ -61,37 +59,28 @@ const initTerminal = async (props: TerminalProps): Promise<void> => {
}); });
const serverSettings = createServerSettings(props); const serverSettings = createServerSettings(props);
createTerminalApp(props, serverSettings);
};
const createTerminalApp = async (props: TerminalProps, serverSettings: ServerConnection.ISettings) => {
const data = { baseUrl: serverSettings.baseUrl }; const data = { baseUrl: serverSettings.baseUrl };
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
try { try {
session = await new JupyterLabAppFactory((restartShell: boolean) => const session = await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
closeTab(props, serverSettings, restartShell),
).createTerminalApp(serverSettings);
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
return session;
} catch (error) { } catch (error) {
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
session = undefined; return undefined;
} }
}; };
const closeTab = (props: TerminalProps, serverSettings: ServerConnection.ISettings, restartShell: boolean): void => { const closeTab = (tabId: string): void => {
if (restartShell) { window.parent.postMessage(
createTerminalApp(props, serverSettings); { type: MessageTypes.CloseTab, data: { tabId: tabId }, signature: "pcIframe" },
} else { window.document.referrer,
window.parent.postMessage( );
{ type: MessageTypes.CloseTab, data: { tabId: props.tabId }, signature: "pcIframe" },
window.document.referrer,
);
}
}; };
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
let session: ITerminalConnection | undefined;
postRobot.on( postRobot.on(
"props", "props",
{ {
@@ -102,7 +91,7 @@ const main = async (): Promise<void> => {
// Typescript definition for event is wrong. So read props by casting to <any> // Typescript definition for event is wrong. So read props by casting to <any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const props = (event as any).data as TerminalProps; const props = (event as any).data as TerminalProps;
await initTerminal(props); session = await initTerminal(props);
}, },
); );

View File

@@ -1,4 +1,3 @@
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils"; import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@@ -48,7 +47,6 @@ export interface VCoreMongoConnectionParams {
} }
interface UserContext { interface UserContext {
readonly fabricDatabaseConnectionInfo?: FabricDatabaseConnectionInfo;
readonly authType?: AuthType; readonly authType?: AuthType;
readonly masterKey?: string; readonly masterKey?: string;
readonly subscriptionId?: string; readonly subscriptionId?: string;

View File

@@ -29,7 +29,7 @@ export function getPriorityLevel(): PriorityLevel {
} }
} }
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, undefined, next) => { export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
if (isRelevantRequest(requestContext)) { if (isRelevantRequest(requestContext)) {
const priorityLevel: PriorityLevel = getPriorityLevel(); const priorityLevel: PriorityLevel = getPriorityLevel();
requestContext.headers["x-ms-cosmos-priority-level"] = priorityLevel as string; requestContext.headers["x-ms-cosmos-priority-level"] = priorityLevel as string;

View File

@@ -1,10 +1,8 @@
import { PartitionKey, PartitionKeyDefinition, PartitionKeyKind } from "@azure/cosmos";
import * as Q from "q"; import * as Q from "q";
import * as sinon from "sinon"; import * as sinon from "sinon";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import * as QueryUtils from "./QueryUtils"; import * as QueryUtils from "./QueryUtils";
import { extractPartitionKeyValues } from "./QueryUtils";
describe("Query Utils", () => { describe("Query Utils", () => {
const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => { const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => {
@@ -96,69 +94,4 @@ describe("Query Utils", () => {
expect(queryStub.getCall(0).args[0]).toBe(0); expect(queryStub.getCall(0).args[0]).toBe(0);
}); });
}); });
describe("extractPartitionKey", () => {
const documentContent = {
"Volcano Name": "Adams",
Country: "United States",
Region: "US-Washington",
Location: {
type: "Point",
coordinates: [-121.49, 46.206],
},
Elevation: 3742,
Type: "Stratovolcano",
Status: "Tephrochronology",
"Last Known Eruption": "Last known eruption from A.D. 1-1499, inclusive",
id: "9e3c494e-8367-3f50-1f56-8c6fcb961363",
_rid: "xzo0AJRYUxUFAAAAAAAAAA==",
_self: "dbs/xzo0AA==/colls/xzo0AJRYUxU=/docs/xzo0AJRYUxUFAAAAAAAAAA==/",
_etag: '"ce00fa43-0000-0100-0000-652840440000"',
_attachments: "attachments/",
_ts: 1697136708,
};
it("should extract single partition key value", () => {
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.Hash,
paths: ["/Elevation"],
};
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
documentContent,
singlePartitionKeyDefinition,
);
expect(partitionKeyValues.length).toBe(1);
expect(partitionKeyValues[0]).toEqual(3742);
});
it("should extract two partition key values", () => {
const multiPartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.MultiHash,
paths: ["/Type", "/Status"],
};
const expectedPartitionKeyValues: string[] = ["Stratovolcano", "Tephrochronology"];
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
documentContent,
multiPartitionKeyDefinition,
);
expect(partitionKeyValues.length).toBe(2);
expect(expectedPartitionKeyValues).toContain(documentContent["Type"]);
expect(expectedPartitionKeyValues).toContain(documentContent["Status"]);
});
it("should extract no partition key values", () => {
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.Hash,
paths: ["/InvalidPartitionKeyPath"],
};
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
documentContent,
singlePartitionKeyDefinition,
);
expect(partitionKeyValues.length).toBe(0);
});
});
}); });

View File

@@ -1,4 +1,3 @@
import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
@@ -83,22 +82,3 @@ export const queryPagesUntilContentPresent = async (
return await doRequest(firstItemIndex); return await doRequest(firstItemIndex);
}; };
/* eslint-disable @typescript-eslint/no-explicit-any */
export const extractPartitionKeyValues = (
documentContent: any,
partitionKeyDefinition: PartitionKeyDefinition,
): PartitionKey[] => {
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0) {
return undefined;
}
const partitionKeyValues: PartitionKey[] = [];
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
const partitionKeyPathWithoutSlash: string = partitionKeyPath.substring(1);
if (documentContent[partitionKeyPathWithoutSlash]) {
partitionKeyValues.push(documentContent[partitionKeyPathWithoutSlash]);
}
});
return partitionKeyValues;
};

View File

@@ -8,7 +8,5 @@
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" /> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
</head> </head>
<body> <body></body>
<div id="Main" style="height: 100%"></div>
</body>
</html> </html>

View File

@@ -1,9 +1,6 @@
import { HttpHeaders } from "Common/Constants";
import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph";
import useSWR from "swr"; import useSWR from "swr";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels"; import { DatabaseAccount } from "../Contracts/DataModels";
/* eslint-disable @typescript-eslint/no-explicit-any */
interface AccountListResult { interface AccountListResult {
nextLink: string; nextLink: string;
@@ -33,56 +30,10 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken:
return accounts.sort((a, b) => a.name.localeCompare(b.name)); return accounts.sort((a, b) => a.name.localeCompare(b.name));
} }
export async function fetchDatabaseAccountsFromGraph(
subscriptionId: string,
accessToken: string,
): Promise<DatabaseAccount[]> {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append("Authorization", bearer);
headers.append(HttpHeaders.contentType, "application/json");
const databaseAccountsQuery = "resources | where type =~ 'microsoft.documentdb/databaseaccounts'";
const apiVersion = "2021-03-01";
const managementResourceGraphAPIURL = `${configContext.ARM_ENDPOINT}providers/Microsoft.ResourceGraph/resources?api-version=${apiVersion}`;
const databaseAccounts: DatabaseAccount[] = [];
let skipToken: string;
do {
const body = {
query: databaseAccountsQuery,
subscriptions: [subscriptionId],
...(skipToken && {
options: {
$skipToken: skipToken,
} as QueryRequestOptions,
}),
};
const response = await fetch(managementResourceGraphAPIURL, {
method: "POST",
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(await response.text());
}
const queryResponse: QueryResponse = (await response.json()) as QueryResponse;
skipToken = queryResponse.$skipToken;
queryResponse.data?.map((databaseAccount: any) => {
databaseAccounts.push(databaseAccount as DatabaseAccount);
});
} while (skipToken);
return databaseAccounts.sort((a, b) => a.name.localeCompare(b.name));
}
export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] | undefined { export function useDatabaseAccounts(subscriptionId: string, armToken: string): DatabaseAccount[] | undefined {
const { data } = useSWR( const { data } = useSWR(
() => (armToken && subscriptionId ? ["databaseAccounts", subscriptionId, armToken] : undefined), () => (armToken && subscriptionId ? ["databaseAccounts", subscriptionId, armToken] : undefined),
(_, subscriptionId, armToken) => fetchDatabaseAccountsFromGraph(subscriptionId, armToken), (_, subscriptionId, armToken) => fetchDatabaseAccounts(subscriptionId, armToken),
); );
return data; return data;
} }

View File

@@ -1,19 +1,16 @@
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { FabricDatabaseConnectionInfo, FabricMessage } from "Contracts/FabricContract"; import { FabricMessage } from "Contracts/FabricContract";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { import { fetchEncryptedToken } from "Platform/Hosted/Components/ConnectExplorer";
handleRequestDatabaseResourceTokensResponse,
scheduleRefreshDatabaseResourceToken,
} from "Platform/Fabric/FabricUtil";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { fetchAccessData } from "hooks/usePortalAccessToken";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { AccountKind, Flights } from "../Common/Constants"; import { AccountKind, Flights } from "../Common/Constants";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { handleCachedDataMessage, sendMessage, sendReadyMessage } from "../Common/MessageHandler"; import { sendMessage, sendReadyMessage } from "../Common/MessageHandler";
import { Platform, configContext, updateConfigContext } from "../ConfigContext"; import { Platform, configContext, updateConfigContext } from "../ConfigContext";
import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts"; import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
@@ -58,21 +55,19 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
userContext.features.phoenixNotebooks = true; userContext.features.phoenixNotebooks = true;
userContext.features.phoenixFeatures = true; userContext.features.phoenixFeatures = true;
} }
let explorer: Explorer;
if (platform === Platform.Hosted) { if (platform === Platform.Hosted) {
explorer = await configureHosted(); const explorer = await configureHosted();
setExplorer(explorer);
} else if (platform === Platform.Emulator) { } else if (platform === Platform.Emulator) {
explorer = configureEmulator(); const explorer = configureEmulator();
setExplorer(explorer);
} else if (platform === Platform.Portal) { } else if (platform === Platform.Portal) {
explorer = await configurePortal(); const explorer = await configurePortal();
setExplorer(explorer);
} else if (platform === Platform.Fabric) { } else if (platform === Platform.Fabric) {
explorer = await configureFabric(); const explorer = await configureFabric();
setExplorer(explorer);
} }
if (explorer && userContext.features.enableCopilot) {
await updateContextForCopilot(explorer);
await updateContextForSampleData(explorer);
}
setExplorer(explorer);
} }
}; };
effect(); effect();
@@ -81,6 +76,9 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
useEffect(() => { useEffect(() => {
if (explorer) { if (explorer) {
applyExplorerBindings(explorer); applyExplorerBindings(explorer);
if (userContext.features.enableCopilot) {
updateContextForSampleData(explorer);
}
} }
}, [explorer]); }, [explorer]);
@@ -102,37 +100,70 @@ async function configureFabric(): Promise<Explorer> {
} }
const data: FabricMessage = event.data?.data; const data: FabricMessage = event.data?.data;
if (!data) { if (!data) {
return; return;
} }
switch (data.type) { switch (data.type) {
case "initialize": { case "initialize": {
const fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo = { // TODO For now, retrieve info from session storage. Replace with info injected into Data Explorer
endpoint: data.message.endpoint, const connectionString = data.connectionString ?? sessionStorage.getItem("connectionString");
databaseId: data.message.databaseId, if (!connectionString) {
resourceTokens: data.message.resourceTokens as { [resourceId: string]: string }, console.error("No connection string found in session storage");
resourceTokensTimestamp: data.message.resourceTokensTimestamp, return undefined;
}; }
explorer = await createExplorerFabric(fabricDatabaseConnectionInfo); const encryptedToken = await fetchEncryptedToken(connectionString);
resolve(explorer); // TODO Duplicated from useTokenMetadata
const encryptedTokenMetadata = await fetchAccessData(encryptedToken);
explorer.refreshAllDatabases().then(() => { const hostedConfig: EncryptedToken = {
openFirstContainer(explorer, fabricDatabaseConnectionInfo.databaseId); authType: AuthType.EncryptedToken,
}); encryptedToken,
scheduleRefreshDatabaseResourceToken(); encryptedTokenMetadata,
};
explorer = await configureWithEncryptedToken(hostedConfig);
resolve(explorer);
break; break;
} }
case "newContainer": case "newContainer":
explorer.onNewCollectionClicked(); explorer.onNewCollectionClicked();
break; break;
case "authorizationToken": { case "openTab": {
handleCachedDataMessage(data); // Expand database first
break; const databaseName = sessionStorage.getItem("openDatabaseName") ?? data.databaseName;
} const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
case "allResourceTokens": { if (database) {
// TODO call handleCachedDataMessage when Fabric echoes message id back await database.expandDatabase();
handleRequestDatabaseResourceTokensResponse(explorer, data.message as FabricDatabaseConnectionInfo); useDatabases.getState().updateDatabase(database);
useSelectedNode.getState().setSelectedNode(database);
let collectionResourceId = data.collectionName;
if (collectionResourceId === undefined) {
// Pick first collection if collectionName not specified in message
collectionResourceId = database.collections()[0]?.id();
}
if (collectionResourceId !== undefined) {
// Expand collection
const collection = database.collections().find((coll) => coll.id() === collectionResourceId);
collection.expandCollection();
useSelectedNode.getState().setSelectedNode(collection);
handleOpenAction(
{
actionType: ActionType.OpenCollectionTab,
databaseResourceId: databaseName,
collectionResourceId: data.collectionName,
tabKind: TabKind.SQLDocuments,
} as DataExplorerAction,
useDatabases.getState().databases,
explorer,
);
}
}
break; break;
} }
default: default:
@@ -147,41 +178,6 @@ async function configureFabric(): Promise<Explorer> {
}); });
} }
const openFirstContainer = async (explorer: Explorer, databaseName: string, collectionName?: string) => {
// Expand database first
databaseName = sessionStorage.getItem("openDatabaseName") ?? databaseName;
const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
if (database) {
await database.expandDatabase();
useDatabases.getState().updateDatabase(database);
useSelectedNode.getState().setSelectedNode(database);
let collectionResourceId = collectionName;
if (collectionResourceId === undefined) {
// Pick first collection if collectionName not specified in message
collectionResourceId = database.collections()[0]?.id();
}
if (collectionResourceId !== undefined) {
// Expand collection
const collection = database.collections().find((coll) => coll.id() === collectionResourceId);
collection.expandCollection();
useSelectedNode.getState().setSelectedNode(collection);
handleOpenAction(
{
actionType: ActionType.OpenCollectionTab,
databaseResourceId: databaseName,
collectionResourceId: collectionName,
tabKind: TabKind.SQLDocuments,
} as DataExplorerAction,
useDatabases.getState().databases,
explorer,
);
}
}
};
async function configureHosted(): Promise<Explorer> { async function configureHosted(): Promise<Explorer> {
const win = window as unknown as HostedExplorerChildFrame; const win = window as unknown as HostedExplorerChildFrame;
let explorer: Explorer; let explorer: Explorer;
@@ -319,25 +315,6 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
return explorer; return explorer;
} }
function createExplorerFabric(fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo): Explorer {
updateUserContext({
fabricDatabaseConnectionInfo,
authType: AuthType.ConnectionString,
databaseAccount: {
id: "",
location: "",
type: "",
name: "Mounted",
kind: AccountKind.Default,
properties: {
documentEndpoint: fabricDatabaseConnectionInfo.endpoint,
},
},
});
const explorer = new Explorer();
return explorer;
}
function configureWithEncryptedToken(config: EncryptedToken): Explorer { function configureWithEncryptedToken(config: EncryptedToken): Explorer {
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
updateUserContext({ updateUserContext({
@@ -420,11 +397,9 @@ async function configurePortal(): Promise<Explorer> {
updateContextsFromPortalMessage(inputs); updateContextsFromPortalMessage(inputs);
explorer = new Explorer(); explorer = new Explorer();
resolve(explorer); resolve(explorer);
if (userContext.apiType === "Postgres") {
if (userContext.apiType === "Postgres" || userContext.apiType === "SQL" || userContext.apiType === "Mongo") { explorer.openNPSSurveyDialog();
setTimeout(() => explorer.openNPSSurveyDialog(), 3000);
} }
if (openAction) { if (openAction) {
handleOpenAction(openAction, useDatabases.getState().databases, explorer); handleOpenAction(openAction, useDatabases.getState().databases, explorer);
} }
@@ -556,23 +531,12 @@ interface PortalMessage {
inputs?: DataExplorerInputsFrame; inputs?: DataExplorerInputsFrame;
} }
async function updateContextForCopilot(explorer: Explorer): Promise<void> {
await explorer.configureCopilot();
}
async function updateContextForSampleData(explorer: Explorer): Promise<void> { async function updateContextForSampleData(explorer: Explorer): Promise<void> {
const copilotEnabled = if (!userContext.features.enableCopilot) {
userContext.apiType === "SQL" && userContext.features.enableCopilot && useQueryCopilot.getState().copilotEnabled;
if (!copilotEnabled) {
return; return;
} }
const sampleDatabaseEndpoint = useQueryCopilot.getState().copilotUserDBEnabled const url = createUri(`${configContext.BACKEND_ENDPOINT}`, `/api/tokens/sampledataconnection`);
? `/api/tokens/sampledataconnection/v2`
: `/api/tokens/sampledataconnection`;
const url = createUri(`${configContext.BACKEND_ENDPOINT}`, sampleDatabaseEndpoint);
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token }; const headers = { [authorizationHeader.header]: authorizationHeader.token };

View File

@@ -1,6 +1,6 @@
import { MinimalQueryIterator } from "Common/IteratorUtilities"; import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { QueryResults } from "Contracts/ViewModels"; import { QueryResults } from "Contracts/ViewModels";
import { CopilotMessage, CopilotSchemaAllocationInfo } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { guid } from "Explorer/Tables/Utilities"; import { guid } from "Explorer/Tables/Utilities";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
@@ -8,8 +8,6 @@ import * as DataModels from "../Contracts/DataModels";
import { ContainerInfo } from "../Contracts/DataModels"; import { ContainerInfo } from "../Contracts/DataModels";
export interface QueryCopilotState { export interface QueryCopilotState {
copilotEnabled: boolean;
copilotUserDBEnabled: boolean;
generatedQuery: string; generatedQuery: string;
likeQuery: boolean; likeQuery: boolean;
userPrompt: string; userPrompt: string;
@@ -36,21 +34,14 @@ export interface QueryCopilotState {
generatedQueryComments: string; generatedQueryComments: string;
wasCopilotUsed: boolean; wasCopilotUsed: boolean;
showWelcomeSidebar: boolean; showWelcomeSidebar: boolean;
showWelcomeModal: boolean;
showCopilotSidebar: boolean; showCopilotSidebar: boolean;
chatMessages: CopilotMessage[]; chatMessages: CopilotMessage[];
shouldIncludeInMessages: boolean; shouldIncludeInMessages: boolean;
showExplanationBubble: boolean; showExplanationBubble: boolean;
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
containerStatus: ContainerInfo; containerStatus: ContainerInfo;
schemaAllocationInfo: CopilotSchemaAllocationInfo;
isAllocatingContainer: boolean; isAllocatingContainer: boolean;
copilotEnabledforExecution: boolean;
getState?: () => QueryCopilotState;
setCopilotEnabled: (copilotEnabled: boolean) => void;
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => void;
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void; openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void;
closeFeedbackModal: () => void; closeFeedbackModal: () => void;
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void; setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void;
@@ -78,7 +69,6 @@ export interface QueryCopilotState {
setGeneratedQueryComments: (generatedQueryComments: string) => void; setGeneratedQueryComments: (generatedQueryComments: string) => void;
setWasCopilotUsed: (wasCopilotUsed: boolean) => void; setWasCopilotUsed: (wasCopilotUsed: boolean) => void;
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => void; setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => void;
setShowWelcomeModal: (showWelcomeModal: boolean) => void;
setShowCopilotSidebar: (showCopilotSidebar: boolean) => void; setShowCopilotSidebar: (showCopilotSidebar: boolean) => void;
setChatMessages: (chatMessages: CopilotMessage[]) => void; setChatMessages: (chatMessages: CopilotMessage[]) => void;
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => void; setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => void;
@@ -86,8 +76,6 @@ export interface QueryCopilotState {
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
setContainerStatus: (containerStatus: ContainerInfo) => void; setContainerStatus: (containerStatus: ContainerInfo) => void;
setIsAllocatingContainer: (isAllocatingContainer: boolean) => void; setIsAllocatingContainer: (isAllocatingContainer: boolean) => void;
setSchemaAllocationInfo: (schemaAllocationInfo: CopilotSchemaAllocationInfo) => void;
setCopilotEnabledforExecution: (copilotEnabledforExecution: boolean) => void;
resetContainerConnection: () => void; resetContainerConnection: () => void;
resetQueryCopilotStates: () => void; resetQueryCopilotStates: () => void;
@@ -96,8 +84,6 @@ export interface QueryCopilotState {
type QueryCopilotStore = UseStore<QueryCopilotState>; type QueryCopilotStore = UseStore<QueryCopilotState>;
export const useQueryCopilot: QueryCopilotStore = create((set) => ({ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
copilotEnabled: false,
copilotUserDBEnabled: false,
generatedQuery: "", generatedQuery: "",
likeQuery: false, likeQuery: false,
userPrompt: "", userPrompt: "",
@@ -124,7 +110,6 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
generatedQueryComments: "", generatedQueryComments: "",
wasCopilotUsed: false, wasCopilotUsed: false,
showWelcomeSidebar: true, showWelcomeSidebar: true,
showWelcomeModal: true,
showCopilotSidebar: false, showCopilotSidebar: false,
chatMessages: [], chatMessages: [],
shouldIncludeInMessages: true, shouldIncludeInMessages: true,
@@ -139,18 +124,11 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
durationLeftInMinutes: undefined, durationLeftInMinutes: undefined,
phoenixServerInfo: undefined, phoenixServerInfo: undefined,
}, },
schemaAllocationInfo: {
databaseId: undefined,
containerId: undefined,
},
isAllocatingContainer: false, isAllocatingContainer: false,
copilotEnabledforExecution: false,
setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }),
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }),
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }), set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ showFeedbackModal: false }), closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }),
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
set({ hideFeedbackModalForLikedQueries }), set({ hideFeedbackModalForLikedQueries }),
refreshCorrelationId: () => set({ correlationId: guid() }), refreshCorrelationId: () => set({ correlationId: guid() }),
@@ -177,7 +155,6 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
setGeneratedQueryComments: (generatedQueryComments: string) => set({ generatedQueryComments }), setGeneratedQueryComments: (generatedQueryComments: string) => set({ generatedQueryComments }),
setWasCopilotUsed: (wasCopilotUsed: boolean) => set({ wasCopilotUsed }), setWasCopilotUsed: (wasCopilotUsed: boolean) => set({ wasCopilotUsed }),
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => set({ showWelcomeSidebar }), setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => set({ showWelcomeSidebar }),
setShowWelcomeModal: (showWelcomeModal: boolean) => set({ showWelcomeModal }),
setShowCopilotSidebar: (showCopilotSidebar: boolean) => set({ showCopilotSidebar }), setShowCopilotSidebar: (showCopilotSidebar: boolean) => set({ showCopilotSidebar }),
setChatMessages: (chatMessages: CopilotMessage[]) => set({ chatMessages }), setChatMessages: (chatMessages: CopilotMessage[]) => set({ chatMessages }),
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }), setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }),
@@ -186,8 +163,6 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
set({ notebookServerInfo }), set({ notebookServerInfo }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }), setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
setIsAllocatingContainer: (isAllocatingContainer: boolean) => set({ isAllocatingContainer }), setIsAllocatingContainer: (isAllocatingContainer: boolean) => set({ isAllocatingContainer }),
setSchemaAllocationInfo: (schemaAllocationInfo: CopilotSchemaAllocationInfo) => set({ schemaAllocationInfo }),
setCopilotEnabledforExecution: (copilotEnabledforExecution: boolean) => set({ copilotEnabledforExecution }),
resetContainerConnection: (): void => { resetContainerConnection: (): void => {
useTabs.getState().closeAllNotebookTabs(true); useTabs.getState().closeAllNotebookTabs(true);
@@ -198,10 +173,6 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
durationLeftInMinutes: undefined, durationLeftInMinutes: undefined,
phoenixServerInfo: undefined, phoenixServerInfo: undefined,
}); });
useQueryCopilot.getState().setSchemaAllocationInfo({
databaseId: undefined,
containerId: undefined,
});
}, },
resetQueryCopilotStates: () => { resetQueryCopilotStates: () => {
@@ -246,10 +217,6 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
durationLeftInMinutes: undefined, durationLeftInMinutes: undefined,
phoenixServerInfo: undefined, phoenixServerInfo: undefined,
}, },
schemaAllocationInfo: {
databaseId: undefined,
containerId: undefined,
},
isAllocatingContainer: false, isAllocatingContainer: false,
})); }));
}, },

View File

@@ -1,9 +1,6 @@
import { HttpHeaders } from "Common/Constants";
import { QueryRequestOptions, QueryResponse } from "Contracts/AzureResourceGraph";
import useSWR from "swr"; import useSWR from "swr";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import { Subscription } from "../Contracts/DataModels"; import { Subscription } from "../Contracts/DataModels";
/* eslint-disable @typescript-eslint/no-explicit-any */
interface SubscriptionListResult { interface SubscriptionListResult {
nextLink: string; nextLink: string;
@@ -35,58 +32,10 @@ export async function fetchSubscriptions(accessToken: string): Promise<Subscript
return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName)); return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName));
} }
export async function fetchSubscriptionsFromGraph(accessToken: string): Promise<Subscription[]> {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append("Authorization", bearer);
headers.append(HttpHeaders.contentType, "application/json");
const subscriptionsQuery =
"resources | where type == 'microsoft.documentdb/databaseaccounts' | join kind=inner ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project subscriptionId, subscriptionName = name, subscriptionState = tostring(parse_json(properties).state) ) on subscriptionId | summarize by subscriptionId, subscriptionName, subscriptionState";
const apiVersion = "2021-03-01";
const managementResourceGraphAPIURL = `${configContext.ARM_ENDPOINT}providers/Microsoft.ResourceGraph/resources?api-version=${apiVersion}`;
const subscriptions: Subscription[] = [];
let skipToken: string;
do {
const body = {
query: subscriptionsQuery,
...(skipToken && {
options: {
$skipToken: skipToken,
} as QueryRequestOptions,
}),
};
const response = await fetch(managementResourceGraphAPIURL, {
method: "POST",
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(await response.text());
}
const queryResponse: QueryResponse = (await response.json()) as QueryResponse;
skipToken = queryResponse.$skipToken;
queryResponse.data?.map((subscription: any) => {
subscriptions.push({
displayName: subscription.subscriptionName,
subscriptionId: subscription.subscriptionId,
state: subscription.subscriptionState,
} as Subscription);
});
} while (skipToken);
return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName));
}
export function useSubscriptions(armToken: string): Subscription[] | undefined { export function useSubscriptions(armToken: string): Subscription[] | undefined {
const { data } = useSWR( const { data } = useSWR(
() => (armToken ? ["subscriptions", armToken] : undefined), () => (armToken ? ["subscriptions", armToken] : undefined),
(_, armToken) => fetchSubscriptionsFromGraph(armToken), (_, armToken) => fetchSubscriptions(armToken),
); );
return data; return data;
} }

View File

@@ -5,7 +5,7 @@ import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
import TabsBase from "../Explorer/Tabs/TabsBase"; import TabsBase from "../Explorer/Tabs/TabsBase";
import { Platform, configContext } from "./../ConfigContext"; import { Platform, configContext } from "./../ConfigContext";
export interface TabsState { interface TabsState {
openedTabs: TabsBase[]; openedTabs: TabsBase[];
openedReactTabs: ReactTabKind[]; openedReactTabs: ReactTabKind[];
activeTab: TabsBase | undefined; activeTab: TabsBase | undefined;

View File

@@ -6,15 +6,6 @@
<mimeMap fileExtension="woff" mimeType="application/font-woff" /> <mimeMap fileExtension="woff" mimeType="application/font-woff" />
</staticContent> </staticContent>
<rewrite> <rewrite>
<rules>
<rule name="AAD-Redirect" stopProcessing="true">
<match url="^aad" ignoreCase="true"/>
<conditions>
<add input="{HTTP_HOST}" pattern="^cosmos.azure.com" />
</conditions>
<action type="Redirect" url="/?feature.enableAadDataPlane=true&amp;feature.disableConnectionStringLogin=true" redirectType="Permanent" />
</rule>
</rules>
<outboundRules> <outboundRules>
<rule name="Strict-Transport-Security" enabled="true"> <rule name="Strict-Transport-Security" enabled="true">
<match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" /> <match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />