mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-30 06:11:38 +00:00
Compare commits
18 Commits
copilot_re
...
users/aisa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd6b6f43e1 | ||
|
|
3a703b0bd0 | ||
|
|
70c031d04b | ||
|
|
b949f60c2a | ||
|
|
deb0ebcf92 | ||
|
|
2d3048eafe | ||
|
|
8075ef2847 | ||
|
|
94158504a8 | ||
|
|
9b032ecae4 | ||
|
|
14d7677056 | ||
|
|
d376a7463c | ||
|
|
9669301d14 | ||
|
|
dcd8d1637b | ||
|
|
f36fccd3ef | ||
|
|
8ff9a84004 | ||
|
|
e42e24b175 | ||
|
|
44d6f29edd | ||
|
|
9db06af552 |
3
images/CopilotCommand.svg
Normal file
3
images/CopilotCommand.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3626 11.9771C13.1573 11.9837 13.019 12.0288 12.9215 12.0836C12.817 12.1424 12.7301 12.229 12.6508 12.3542C12.4802 12.6239 12.3837 12.9957 12.252 13.5033L12.2347 13.5699C12.1079 14.0581 11.9424 14.6764 11.5775 15.1594C11.1788 15.687 10.5903 16 9.74835 16H4.63681C3.55423 16 2.75741 15.2435 2.18373 14.3271C1.60629 13.4047 1.17786 12.2045 0.845481 11.0542L0.845003 11.0525C0.464365 9.73512 -0.017355 8.06787 0.000479698 6.72244C0.00951195 6.04103 0.145994 5.34953 0.569528 4.825C1.00712 4.28305 1.66518 4.02294 2.50365 4.02294H2.63739C2.84267 4.01631 2.98102 3.97123 3.07848 3.9164C3.18305 3.85757 3.26995 3.77102 3.34916 3.64584C3.51985 3.37613 3.61631 3.00432 3.74801 2.49671L3.76529 2.43012C3.89213 1.94191 4.05757 1.32365 4.42251 0.840633C4.82119 0.312966 5.40972 0 6.25165 0H11.3632C12.4458 0 13.2426 0.756543 13.8163 1.67295C14.3937 2.59534 14.8221 3.79546 15.1545 4.94582L15.155 4.94748C15.5356 6.26488 16.0174 7.93213 15.9995 9.27756C15.9905 9.95897 15.854 10.6505 15.4305 11.175C14.9929 11.717 14.3348 11.9771 13.4963 11.9771H13.3626ZM14.1354 5.28381C13.8114 4.16251 13.4194 3.09096 12.9303 2.3096C12.4374 1.52225 11.9215 1.14296 11.3632 1.14296H7.95613C8.10274 1.42082 8.22537 1.731 8.33548 2.05199C8.48008 2.47355 8.61427 2.94842 8.75233 3.43699L8.78302 3.54557C9.32758 5.47082 10.008 7.94794 10.4498 9.5646C10.6507 10.2998 11.2693 10.8098 11.9798 10.8333H13.4156C13.4253 10.8333 13.4351 10.8335 13.4447 10.8341H13.4963C14.1298 10.8341 14.4484 10.6443 14.6237 10.4272C14.813 10.1928 14.9255 9.8162 14.9328 9.26133C14.9477 8.13479 14.5323 6.65749 14.1354 5.28381ZM3.06972 13.6904C3.56261 14.4777 4.0785 14.857 4.63681 14.857H8.04387C7.89726 14.5792 7.77463 14.269 7.66452 13.948C7.51992 13.5265 7.38573 13.0516 7.24767 12.563L7.21698 12.4544C6.67242 10.5292 5.99197 8.05206 5.55019 6.4354C5.34929 5.70024 4.73069 5.19015 4.02018 5.16674H2.58445C2.57465 5.16674 2.56492 5.16646 2.55526 5.1659H2.50365C1.87025 5.1659 1.5516 5.35572 1.37634 5.57277C1.18703 5.80723 1.07454 6.1838 1.06719 6.73867C1.05225 7.86521 1.46769 9.34251 1.86459 10.7162C2.18857 11.8375 2.58057 12.909 3.06972 13.6904ZM9.10298 10.8333H9.87304C9.67506 10.5554 9.5217 10.236 9.42598 9.88575C9.233 9.17956 8.99466 8.30985 8.74215 7.39421L8.47331 6.42622C8.26579 5.67902 7.62465 5.16674 6.89702 5.16674H6.12695C6.32494 5.44463 6.4783 5.76395 6.57402 6.11425C6.767 6.82044 7.00534 7.69015 7.25785 8.60579L7.52669 9.57378C7.73421 10.321 8.37535 10.8333 9.10298 10.8333ZM6.89702 4.02378C7.23212 4.02378 7.55609 4.08968 7.85646 4.21152C7.82459 4.0984 7.7931 3.98682 7.76205 3.87706L7.73417 3.77844C7.59304 3.27914 7.46754 2.83514 7.33414 2.44626C7.19129 2.02979 7.05211 1.71602 6.90214 1.4942C6.77014 1.29897 6.56087 1.14296 6.25165 1.14296C5.69009 1.14296 5.42306 1.33297 5.2517 1.55976C5.04661 1.83121 4.92755 2.21889 4.79304 2.73662C4.78284 2.77588 4.77249 2.81622 4.76191 2.85744C4.66966 3.21703 4.55997 3.64464 4.37733 4.02378H6.89702ZM9.10298 11.9762C8.76788 11.9762 8.44391 11.9103 8.14354 11.7885C8.1754 11.9016 8.2069 12.0132 8.23795 12.1229L8.26583 12.2216C8.40696 12.7208 8.53246 13.1649 8.66585 13.5537C8.80871 13.9702 8.94789 14.284 9.09786 14.5058C9.22986 14.701 9.43913 14.857 9.74835 14.857C10.3099 14.857 10.5769 14.667 10.7483 14.4402C10.9534 14.1688 11.0725 13.7811 11.207 13.2634C11.2172 13.2241 11.2275 13.1838 11.2381 13.1426C11.3303 12.783 11.44 12.3554 11.6227 11.9762H9.10298Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -1179,16 +1179,16 @@ menuQuickStart {
|
||||
}
|
||||
}
|
||||
|
||||
.gridRowSelected {
|
||||
#tbodycontent tr.gridRowSelected {
|
||||
.active();
|
||||
}
|
||||
|
||||
.gridRowSelected:hover {
|
||||
#tbodycontent tr.gridRowSelected:hover {
|
||||
cursor: default;
|
||||
.hover();
|
||||
}
|
||||
|
||||
.gridRowHighlighted {
|
||||
#tbodycontent tr.gridRowHighlighted {
|
||||
border-style: dotted;
|
||||
border-width: 2px;
|
||||
}
|
||||
@@ -2576,9 +2576,10 @@ a:link {
|
||||
.querydropdown.placeholderVisible {
|
||||
font-style: italic;
|
||||
}
|
||||
.querydropdown.placeholderVisible::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||
.querydropdown.placeholderVisible::placeholder {
|
||||
/* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||
color: #767474;
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.querydropdown:hover {
|
||||
@@ -2648,7 +2649,7 @@ a:link {
|
||||
|
||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
|
||||
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 {
|
||||
@@ -3096,4 +3097,3 @@ a:link {
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -179,10 +179,11 @@
|
||||
}
|
||||
},
|
||||
"@azure/cosmos": {
|
||||
"version": "3.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.16.2.tgz",
|
||||
"integrity": "sha512-sceY5LWj0BHGj8PSyaVCfDRQLVZyoCfIY78kyIROJVEw0k+p9XFs8fhpykN8JklkCftL0WlaVY+X25SQwnhZsw==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/@azure/cosmos/-/cosmos-4.0.0.tgz",
|
||||
"integrity": "sha1-X9qLNctiu82lIVm5bEw5gahD1bk=",
|
||||
"requires": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-auth": "^1.3.0",
|
||||
"@azure/core-rest-pipeline": "^1.2.0",
|
||||
"debug": "^4.1.1",
|
||||
@@ -22133,6 +22134,12 @@
|
||||
"resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz",
|
||||
"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": {
|
||||
"version": "12.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.16.2",
|
||||
"@azure/cosmos": "4.0.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.2.1",
|
||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
||||
@@ -92,6 +92,7 @@
|
||||
"react-notification-system": "0.2.17",
|
||||
"react-redux": "7.1.3",
|
||||
"react-splitter-layout": "4.0.0",
|
||||
"react-string-format": "1.0.1",
|
||||
"react-youtube": "9.0.1",
|
||||
"redux": "4.0.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
@@ -234,4 +235,4 @@
|
||||
"printWidth": 120,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ export class HttpHeaders {
|
||||
public static activityId: string = "x-ms-activity-id";
|
||||
public static apiType: string = "x-ms-cosmos-apitype";
|
||||
public static authorization: string = "authorization";
|
||||
public static graphAuthorization: string = "graph-authorization";
|
||||
public static collectionIndexTransformationProgress: string =
|
||||
"x-ms-documentdb-collection-index-transformation-progress";
|
||||
public static continuation: string = "x-ms-continuation";
|
||||
|
||||
@@ -125,7 +125,7 @@ describe("requestPlugin", () => {
|
||||
const headers = {};
|
||||
const endpoint = "https://docs.azure.com";
|
||||
const path = "/dbs/foo";
|
||||
requestPlugin({ endpoint, headers, path } as any, next as any);
|
||||
requestPlugin({ endpoint, headers, path } as any, undefined, next as any);
|
||||
expect(next.mock.calls[0][0]).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -137,7 +137,7 @@ describe("requestPlugin", () => {
|
||||
const headers = {};
|
||||
const endpoint = "";
|
||||
const path = "/dbs/foo";
|
||||
requestPlugin({ endpoint, headers, path } as any, next as any);
|
||||
requestPlugin({ endpoint, headers, path } as any, undefined, next as any);
|
||||
expect(next.mock.calls[0][0]).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||
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 { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||
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;
|
||||
|
||||
@@ -26,6 +29,36 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
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) {
|
||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
@@ -41,7 +74,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
return decodeURIComponent(result.PrimaryReadWriteToken);
|
||||
};
|
||||
|
||||
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
|
||||
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, diagnosticNode, next) => {
|
||||
requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href;
|
||||
requestContext.headers["x-ms-proxy-target"] = endpoint();
|
||||
return next(requestContext);
|
||||
@@ -56,7 +89,11 @@ export const endpoint = () => {
|
||||
return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
|
||||
};
|
||||
|
||||
export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> {
|
||||
export async function getTokenFromAuthService(
|
||||
verb: string,
|
||||
resourceType: string,
|
||||
resourceId?: string,
|
||||
): Promise<AuthorizationToken> {
|
||||
try {
|
||||
const host = configContext.BACKEND_ENDPOINT;
|
||||
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
||||
|
||||
@@ -22,7 +22,7 @@ export function handleCachedDataMessage(message: any): void {
|
||||
if (messageContent.error != null) {
|
||||
cachedDataPromise.deferred.reject(messageContent.error);
|
||||
} else {
|
||||
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
|
||||
cachedDataPromise.deferred.resolve(messageContent.data);
|
||||
}
|
||||
runGarbageCollector();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,50 @@
|
||||
import { ContainerResponse } from "@azure/cosmos";
|
||||
import { Queries } from "Common/Constants";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { listSqlContainers } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { listTables } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
|
||||
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}`);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,17 +1,45 @@
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { listSqlDatabases } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
let databases: DataModels.Database[];
|
||||
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("/");
|
||||
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: [],
|
||||
}));
|
||||
return databases;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
|
||||
66
src/Common/getAuthorizationTokenUsingResourceTokens.ts
Normal file
66
src/Common/getAuthorizationTokenUsingResourceTokens.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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, "");
|
||||
}
|
||||
@@ -64,6 +64,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
|
||||
@@ -1,46 +1,6 @@
|
||||
import { MessageTypes } from "Contracts/MessageTypes";
|
||||
import * as ActionContracts from "./ActionContracts";
|
||||
import * as Diagnostics from "./Diagnostics";
|
||||
import * as Versions from "./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 };
|
||||
export { ActionContracts, Diagnostics, MessageTypes, Versions };
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { AuthorizationToken, MessageTypes } from "./MessageTypes";
|
||||
|
||||
export type FabricMessage =
|
||||
| {
|
||||
type: "newContainer";
|
||||
@@ -5,21 +7,71 @@ export type FabricMessage =
|
||||
}
|
||||
| {
|
||||
type: "initialize";
|
||||
connectionString: string | undefined;
|
||||
message: {
|
||||
endpoint: string | undefined;
|
||||
databaseId: string | undefined;
|
||||
resourceTokens: unknown | undefined;
|
||||
resourceTokensTimestamp: number | undefined;
|
||||
error: string | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "openTab";
|
||||
databaseName: string;
|
||||
collectionName: string | undefined;
|
||||
type: "authorizationToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: AuthorizationToken | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "allResourceTokens";
|
||||
message: {
|
||||
endpoint: string | undefined;
|
||||
databaseId: string | undefined;
|
||||
resourceTokens: unknown | undefined;
|
||||
resourceTokensTimestamp: number | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export type DataExploreMessage =
|
||||
| "ready"
|
||||
| {
|
||||
type: number;
|
||||
type: MessageTypes.TelemetryInfo;
|
||||
data: {
|
||||
action: "LoadDatabases";
|
||||
actionModifier: "success" | "start";
|
||||
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;
|
||||
}
|
||||
|
||||
49
src/Contracts/MessageTypes.ts
Normal file
49
src/Contracts/MessageTypes.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
4
src/Definitions/less.d.ts
vendored
Normal file
4
src/Definitions/less.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.less" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
@@ -129,20 +129,22 @@ export const createCollectionContextMenuButton = (
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: () => {
|
||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
});
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: () => {
|
||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -123,19 +123,6 @@ describe("ContainerSampleGenerator", () => {
|
||||
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 () => {
|
||||
const experience = "Sample generation not supported for this API Tables";
|
||||
updateUserContext({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { sendMessage } from "Common/MessageHandler";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import * as ko from "knockout";
|
||||
@@ -379,6 +380,13 @@ export default class Explorer {
|
||||
};
|
||||
|
||||
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
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
|
||||
@@ -50,31 +50,36 @@ export function createStaticCommandBarButtons(
|
||||
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
||||
}
|
||||
|
||||
const newCollectionBtn = createNewCollectionGroup(container);
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
buttons.push(newCollectionBtn);
|
||||
if (
|
||||
configContext.platform !== Platform.Fabric &&
|
||||
userContext.apiType !== "Tables" &&
|
||||
userContext.apiType !== "Cassandra"
|
||||
) {
|
||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||
|
||||
if (addSynapseLink) {
|
||||
// Avoid starting with a divider
|
||||
const addDivider = () => {
|
||||
if (buttons.length > 0) {
|
||||
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) {
|
||||
buttons.push(createDivider());
|
||||
addDivider();
|
||||
const notebookButtons: CommandButtonComponentProps[] = [];
|
||||
|
||||
const newNotebookButton = createNewNotebookButton(container);
|
||||
@@ -128,7 +133,7 @@ export function createStaticCommandBarButtons(
|
||||
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
|
||||
if (isQuerySupported) {
|
||||
buttons.push(createDivider());
|
||||
addDivider();
|
||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||
buttons.push(newSqlQueryBtn);
|
||||
}
|
||||
|
||||
@@ -114,7 +114,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
|
||||
createNewDatabase:
|
||||
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
||||
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
||||
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
||||
selectedDatabaseId:
|
||||
@@ -274,36 +275,38 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.createNewDatabase}
|
||||
aria-label="Create new database"
|
||||
aria-checked={this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="databaseCreateNew"
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
{configContext.platform !== Platform.Fabric && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.createNewDatabase}
|
||||
aria-label="Create new database"
|
||||
aria-checked={this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="databaseCreateNew"
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.createNewDatabase}
|
||||
aria-label="Use existing database"
|
||||
aria-checked={!this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
</div>
|
||||
</Stack>
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.createNewDatabase}
|
||||
aria-label="Use existing database"
|
||||
aria-checked={!this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{this.state.createNewDatabase && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
|
||||
@@ -32,14 +32,8 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||
return (
|
||||
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
||||
{icon}
|
||||
<span className="panelWarningErrorDetailsLinkContainer">
|
||||
<Text
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-label={message}
|
||||
className="panelWarningErrorMessage"
|
||||
variant="small"
|
||||
>
|
||||
<span className="panelWarningErrorDetailsLinkContainer" role="alert" aria-live="assertive">
|
||||
<Text aria-label={message} className="panelWarningErrorMessage" variant="small">
|
||||
{message}
|
||||
{link && linkText && (
|
||||
<Link target="_blank" href={link}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
import { ReactWrapper, mount } from "enzyme";
|
||||
import React from "react";
|
||||
import { RightPaneForm } from "./RightPaneForm";
|
||||
|
||||
@@ -34,6 +34,6 @@ describe("Right Pane Form", () => {
|
||||
it("should render error in header", () => {
|
||||
render(<RightPaneForm {...props} formError="file already Exist" />);
|
||||
expect(screen.getByLabelText("error")).toBeDefined();
|
||||
expect(screen.getByRole("alert").innerHTML).toEqual("file already Exist");
|
||||
expect(screen.getByRole("alert").innerHTML).toContain("file already Exist");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
|
||||
import {
|
||||
Checkbox,
|
||||
ChoiceGroup,
|
||||
IChoiceGroupOption,
|
||||
ISpinButtonStyles,
|
||||
IToggleStyles,
|
||||
Position,
|
||||
SpinButton,
|
||||
Toggle,
|
||||
} from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { configContext } from "ConfigContext";
|
||||
@@ -6,10 +15,10 @@ import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import * as StringUtility from "Shared/StringUtility";
|
||||
import { userContext } from "UserContext";
|
||||
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { FunctionComponent, MouseEvent, useState } from "react";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
export const SettingsPane: FunctionComponent = () => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
@@ -19,6 +28,13 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
? Constants.Queries.UnlimitedPageOption
|
||||
: 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>(
|
||||
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0,
|
||||
);
|
||||
@@ -53,7 +69,7 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
||||
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
const handlerOnSubmit = () => {
|
||||
setIsExecuting(true);
|
||||
|
||||
LocalStorageUtility.setEntryNumber(
|
||||
@@ -61,6 +77,7 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
||||
);
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
||||
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
|
||||
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
||||
@@ -73,6 +90,14 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (queryTimeoutEnabled) {
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout);
|
||||
LocalStorageUtility.setEntryBoolean(
|
||||
StorageKey.AutomaticallyCancelQueryAfterTimeout,
|
||||
automaticallyCancelQueryAfterTimeout,
|
||||
);
|
||||
}
|
||||
|
||||
setIsExecuting(false);
|
||||
logConsoleInfo(
|
||||
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
|
||||
@@ -97,7 +122,6 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
||||
);
|
||||
closeSidePanel();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const isCustomPageOptionSelected = () => {
|
||||
@@ -112,7 +136,7 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
formError: "",
|
||||
isExecuting,
|
||||
submitButtonText: "Apply",
|
||||
onSubmit: () => handlerOnSubmit(undefined),
|
||||
onSubmit: () => handlerOnSubmit(),
|
||||
};
|
||||
const pageOptionList: IChoiceGroupOption[] = [
|
||||
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
|
||||
@@ -140,6 +164,21 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
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 = {
|
||||
root: {
|
||||
clear: "both",
|
||||
@@ -161,6 +200,35 @@ 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 (
|
||||
<RightPaneForm {...genericPaneProps}>
|
||||
<div className="paneMainContent">
|
||||
@@ -211,6 +279,50 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
</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="settingsSectionPart">
|
||||
<div className="settingsSectionLabel">
|
||||
|
||||
@@ -97,6 +97,46 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
</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
|
||||
className="settingsSection"
|
||||
>
|
||||
|
||||
@@ -390,6 +390,9 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
<div className="panelNullWarning" style={{ padding: "20px", color: "red" }}>
|
||||
Warning: Null fields will not be displayed for editing.
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -355,6 +355,17 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
<div
|
||||
className="panelNullWarning"
|
||||
style={
|
||||
Object {
|
||||
"color": "red",
|
||||
"padding": "20px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Warning: Null fields will not be displayed for editing.
|
||||
</div>
|
||||
<PanelFooterComponent
|
||||
buttonLabel="Update"
|
||||
isButtonDisabled={false}
|
||||
|
||||
@@ -323,21 +323,19 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
</IconBase>
|
||||
</StyledIconBase>
|
||||
<span
|
||||
aria-live="assertive"
|
||||
className="panelWarningErrorDetailsLinkContainer"
|
||||
key=".0:$.1"
|
||||
role="alert"
|
||||
>
|
||||
<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-live="assertive"
|
||||
className="panelWarningErrorMessage"
|
||||
role="alert"
|
||||
variant="small"
|
||||
>
|
||||
<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-live="assertive"
|
||||
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.
|
||||
</span>
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should submit submission", () => {
|
||||
it("should not submit submission if required description field is null", () => {
|
||||
const explorer = new Explorer();
|
||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
|
||||
|
||||
@@ -110,12 +110,24 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||
submitButton.simulate("click");
|
||||
wrapper.setProps({});
|
||||
|
||||
expect(SubmitFeedback).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("should submit submission", () => {
|
||||
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
|
||||
const explorer = new Explorer();
|
||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
|
||||
|
||||
const submitButton = wrapper.find("form");
|
||||
submitButton.simulate("submit");
|
||||
wrapper.setProps({});
|
||||
|
||||
expect(SubmitFeedback).toHaveBeenCalledTimes(1);
|
||||
expect(SubmitFeedback).toHaveBeenCalledWith({
|
||||
params: {
|
||||
likeQuery: false,
|
||||
generatedQuery: "",
|
||||
userPrompt: "",
|
||||
generatedQuery: "test query",
|
||||
userPrompt: "test prompt",
|
||||
description: "",
|
||||
contact: getUserEmail(),
|
||||
},
|
||||
|
||||
@@ -25,93 +25,94 @@ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }):
|
||||
closeFeedbackModal,
|
||||
setHideFeedbackModalForLikedQueries,
|
||||
} = useQueryCopilot();
|
||||
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(true);
|
||||
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(false);
|
||||
const [description, setDescription] = React.useState<string>("");
|
||||
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, contact },
|
||||
explorer: explorer,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={showFeedbackModal}>
|
||||
<Stack style={{ padding: 24 }}>
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
|
||||
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
|
||||
</Stack>
|
||||
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
|
||||
<TextField
|
||||
styles={{ root: { marginBottom: 14 } }}
|
||||
label="Description"
|
||||
required
|
||||
placeholder="Provide more details"
|
||||
value={description}
|
||||
onChange={(_, newValue) => setDescription(newValue)}
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
<TextField
|
||||
styles={{ root: { marginBottom: 14 } }}
|
||||
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)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack style={{ padding: 24 }}>
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
|
||||
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
|
||||
</Stack>
|
||||
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
|
||||
<TextField
|
||||
styles={{ root: { marginBottom: 14 } }}
|
||||
label="Description"
|
||||
required
|
||||
placeholder="Provide more details"
|
||||
value={description}
|
||||
onChange={(_, newValue) => setDescription(newValue)}
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
)}
|
||||
<Stack horizontal horizontalAlign="end">
|
||||
<PrimaryButton
|
||||
styles={{ root: { marginRight: 8 } }}
|
||||
onClick={() => {
|
||||
closeFeedbackModal();
|
||||
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
||||
SubmitFeedback({
|
||||
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
||||
explorer: explorer,
|
||||
});
|
||||
<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 },
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</PrimaryButton>
|
||||
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
|
||||
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 } }} type="submit">
|
||||
Submit
|
||||
</PrimaryButton>
|
||||
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||
</Stack>
|
||||
<Stack horizontalAlign="center">
|
||||
<Stack.Item align="center" style={{ textAlign: "center" }}>
|
||||
<Text className="title bold">Welcome to Query Copilot for Azure Cosmos DB (Private Preview)</Text>
|
||||
<Text className="title bold">Welcome to Copilot in Azure Cosmos DB (Private Preview)</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item align="center" className="text">
|
||||
<Stack horizontal>
|
||||
@@ -61,7 +61,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
||||
</StackItem>
|
||||
<StackItem align="start">
|
||||
<Text className="bold">
|
||||
Let Query Copilot do the work for you
|
||||
Let Copilot do the work for you
|
||||
<br />
|
||||
</Text>
|
||||
</StackItem>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||
<Text
|
||||
className="title bold"
|
||||
>
|
||||
Welcome to Query Copilot for Azure Cosmos DB (Private Preview)
|
||||
Welcome to Copilot in Azure Cosmos DB (Private Preview)
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
@@ -100,7 +100,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
|
||||
<Text
|
||||
className="bold"
|
||||
>
|
||||
Let Query Copilot do the work for you
|
||||
Let Copilot do the work for you
|
||||
<br />
|
||||
</Text>
|
||||
</StackItem>
|
||||
|
||||
567
src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx
Normal file
567
src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx
Normal file
@@ -0,0 +1,567 @@
|
||||
import {
|
||||
Callout,
|
||||
CommandBarButton,
|
||||
DefaultButton,
|
||||
DirectionalHint,
|
||||
IButtonStyles,
|
||||
IconButton,
|
||||
Image,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Separator,
|
||||
Spinner,
|
||||
Stack,
|
||||
TeachingBubble,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import {
|
||||
ContainerStatusType,
|
||||
PoolIdType,
|
||||
QueryCopilotSampleContainerSchema,
|
||||
ShortenedQueryCopilotSampleContainerSchema,
|
||||
} from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
||||
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
||||
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
||||
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
|
||||
import { userContext } from "UserContext";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React, { useRef, useState } from "react";
|
||||
import HintIcon from "../../../images/Hint.svg";
|
||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||
import RecentIcon from "../../../images/Recent.svg";
|
||||
import errorIcon from "../../../images/close-black.svg";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
|
||||
type QueryCopilotPromptProps = QueryCopilotProps & {
|
||||
toggleCopilot: (toggle: boolean) => void;
|
||||
};
|
||||
|
||||
interface SuggestedPrompt {
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const promptStyles: IButtonStyles = {
|
||||
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
|
||||
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
||||
};
|
||||
|
||||
export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
explorer,
|
||||
toggleCopilot,
|
||||
}: QueryCopilotPromptProps): JSX.Element => {
|
||||
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
||||
const inputEdited = useRef(false);
|
||||
const {
|
||||
hideFeedbackModalForLikedQueries,
|
||||
userPrompt,
|
||||
setUserPrompt,
|
||||
generatedQuery,
|
||||
setGeneratedQuery,
|
||||
query,
|
||||
setQuery,
|
||||
isGeneratingQuery,
|
||||
setIsGeneratingQuery,
|
||||
likeQuery,
|
||||
setLikeQuery,
|
||||
dislikeQuery,
|
||||
setDislikeQuery,
|
||||
showCallout,
|
||||
setShowCallout,
|
||||
isSamplePromptsOpen,
|
||||
setIsSamplePromptsOpen,
|
||||
showSamplePrompts,
|
||||
setShowSamplePrompts,
|
||||
showDeletePopup,
|
||||
setShowDeletePopup,
|
||||
showFeedbackBar,
|
||||
setShowFeedbackBar,
|
||||
showCopyPopup,
|
||||
setshowCopyPopup,
|
||||
showErrorMessageBar,
|
||||
showInvalidQueryMessageBar,
|
||||
setShowInvalidQueryMessageBar,
|
||||
setShowErrorMessageBar,
|
||||
setGeneratedQueryComments,
|
||||
setQueryResults,
|
||||
setErrorMessage,
|
||||
} = useQueryCopilot();
|
||||
|
||||
const sampleProps: SamplePromptsProps = {
|
||||
isSamplePromptsOpen: isSamplePromptsOpen,
|
||||
setIsSamplePromptsOpen: setIsSamplePromptsOpen,
|
||||
setTextBox: setUserPrompt,
|
||||
};
|
||||
|
||||
const copyGeneratedCode = () => {
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
const queryElement = document.createElement("textarea");
|
||||
queryElement.value = query;
|
||||
document.body.appendChild(queryElement);
|
||||
queryElement.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(queryElement);
|
||||
|
||||
setshowCopyPopup(true);
|
||||
setTimeout(() => {
|
||||
setshowCopyPopup(false);
|
||||
}, 6000);
|
||||
};
|
||||
|
||||
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
||||
const cachedHistories = cachedHistoriesString?.split("|");
|
||||
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
||||
const suggestedPrompts: SuggestedPrompt[] = [
|
||||
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
|
||||
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
|
||||
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
|
||||
];
|
||||
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
|
||||
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
|
||||
|
||||
const handleUserPromptChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
inputEdited.current = true;
|
||||
const { value } = event.target;
|
||||
setUserPrompt(value);
|
||||
|
||||
// Filter history prompts
|
||||
const filteredHistory = histories.filter((history) => history.toLowerCase().includes(value.toLowerCase()));
|
||||
setFilteredHistories(filteredHistory);
|
||||
|
||||
// Filter suggested prompts
|
||||
const filteredSuggested = suggestedPrompts.filter((prompt) =>
|
||||
prompt.text.toLowerCase().includes(value.toLowerCase()),
|
||||
);
|
||||
setFilteredSuggestedPrompts(filteredSuggested);
|
||||
};
|
||||
|
||||
const updateHistories = (): void => {
|
||||
const formattedUserPrompt = userPrompt.replace(/\s+/g, " ").trim();
|
||||
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
|
||||
|
||||
const updatedHistories = existingHistories.filter(
|
||||
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase(),
|
||||
);
|
||||
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||
|
||||
setHistories(newHistories);
|
||||
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
|
||||
};
|
||||
|
||||
const resetMessageStates = (): void => {
|
||||
setShowErrorMessageBar(false);
|
||||
setShowInvalidQueryMessageBar(false);
|
||||
setShowFeedbackBar(false);
|
||||
};
|
||||
|
||||
const resetQueryResults = (): void => {
|
||||
setQueryResults(null);
|
||||
setErrorMessage("");
|
||||
};
|
||||
|
||||
const generateSQLQuery = async (): Promise<void> => {
|
||||
try {
|
||||
resetMessageStates();
|
||||
setIsGeneratingQuery(true);
|
||||
setShowDeletePopup(false);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
if (
|
||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
||||
!userContext.features.disableCopilotPhoenixGateaway
|
||||
) {
|
||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
||||
}
|
||||
const payload = {
|
||||
containerSchema: userContext.features.enableCopilotFullSchema
|
||||
? QueryCopilotSampleContainerSchema
|
||||
: ShortenedQueryCopilotSampleContainerSchema,
|
||||
userPrompt: userPrompt,
|
||||
};
|
||||
useQueryCopilot.getState().refreshCorrelationId();
|
||||
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
|
||||
const queryUri = userContext.features.disableCopilotPhoenixGateaway
|
||||
? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery")
|
||||
: createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery");
|
||||
const response = await fetch(queryUri, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||
if (response.ok) {
|
||||
if (generateSQLQueryResponse?.sql !== "N/A") {
|
||||
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
||||
if (generateSQLQueryResponse.explanation) {
|
||||
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
||||
}
|
||||
query += generateSQLQueryResponse.sql;
|
||||
setQuery(query);
|
||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||
setShowFeedbackBar(true);
|
||||
resetQueryResults();
|
||||
} else {
|
||||
setShowInvalidQueryMessageBar(true);
|
||||
}
|
||||
} else {
|
||||
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
|
||||
useTabs.getState().setIsQueryErrorThrown(true);
|
||||
setShowErrorMessageBar(true);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "executeNaturalLanguageQuery");
|
||||
useTabs.getState().setIsQueryErrorThrown(true);
|
||||
setShowErrorMessageBar(true);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsGeneratingQuery(false);
|
||||
useTabs.getState().setIsTabExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const showTeachingBubble = (): void => {
|
||||
if (!inputEdited.current) {
|
||||
setTimeout(() => {
|
||||
if (!inputEdited.current && !isWelcomModalVisible()) {
|
||||
toggleCopilotTeachingBubbleVisible();
|
||||
inputEdited.current = true;
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
};
|
||||
|
||||
const isWelcomModalVisible = (): boolean => {
|
||||
return localStorage.getItem("hideWelcomeModal") !== "true";
|
||||
};
|
||||
|
||||
const clearFeedback = () => {
|
||||
resetButtonState();
|
||||
resetQueryResults();
|
||||
};
|
||||
|
||||
const resetButtonState = () => {
|
||||
setDislikeQuery(false);
|
||||
setLikeQuery(false);
|
||||
setShowCallout(false);
|
||||
};
|
||||
|
||||
const startGenerateQueryProcess = () => {
|
||||
updateHistories();
|
||||
generateSQLQuery();
|
||||
resetButtonState();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
showTeachingBubble();
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack className="copilot-prompt-pane" styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}>
|
||||
<Stack horizontal>
|
||||
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} />
|
||||
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
||||
<IconButton
|
||||
iconProps={{ imageProps: { src: errorIcon } }}
|
||||
onClick={() => {
|
||||
toggleCopilot(false);
|
||||
clearFeedback();
|
||||
resetMessageStates();
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
marginLeft: "auto !important",
|
||||
},
|
||||
}}
|
||||
ariaLabel="Close"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<TextField
|
||||
id="naturalLanguageInput"
|
||||
value={userPrompt}
|
||||
onChange={handleUserPromptChange}
|
||||
onClick={() => {
|
||||
inputEdited.current = true;
|
||||
setShowSamplePrompts(true);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && userPrompt) {
|
||||
inputEdited.current = true;
|
||||
startGenerateQueryProcess();
|
||||
}
|
||||
}}
|
||||
style={{ lineHeight: 30 }}
|
||||
styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }}
|
||||
disabled={isGeneratingQuery}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{copilotTeachingBubbleVisible && (
|
||||
<TeachingBubble
|
||||
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
||||
target="#naturalLanguageInput"
|
||||
hasCloseButton={true}
|
||||
closeButtonAriaLabel="Close"
|
||||
onDismiss={toggleCopilotTeachingBubbleVisible}
|
||||
hasSmallHeadline={true}
|
||||
headline="Write a prompt"
|
||||
>
|
||||
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowSamplePrompts(true);
|
||||
toggleCopilotTeachingBubbleVisible();
|
||||
}}
|
||||
style={{ color: "white", fontWeight: 600 }}
|
||||
>
|
||||
sample prompts
|
||||
</Link>{" "}
|
||||
or write your own query
|
||||
</TeachingBubble>
|
||||
)}
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => startGenerateQueryProcess()}
|
||||
/>
|
||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||
{showSamplePrompts && (
|
||||
<Callout
|
||||
styles={{ root: { minWidth: 400 } }}
|
||||
target="#naturalLanguageInput"
|
||||
isBeakVisible={false}
|
||||
onDismiss={() => setShowSamplePrompts(false)}
|
||||
directionalHintFixed={true}
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
alignTargetEdge={true}
|
||||
gapSpace={4}
|
||||
>
|
||||
<Stack>
|
||||
{filteredHistories?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Recent
|
||||
</Text>
|
||||
{filteredHistories.map((history, i) => (
|
||||
<DefaultButton
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setUserPrompt(history);
|
||||
setShowSamplePrompts(false);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
onRenderIcon={() => <Image src={RecentIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{history}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{filteredSuggestedPrompts?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Suggested Prompts
|
||||
</Text>
|
||||
{filteredSuggestedPrompts.map((prompt) => (
|
||||
<DefaultButton
|
||||
key={prompt.id}
|
||||
onClick={() => {
|
||||
setUserPrompt(prompt.text);
|
||||
setShowSamplePrompts(false);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
onRenderIcon={() => <Image src={HintIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{prompt.text}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
|
||||
<Stack>
|
||||
<Separator
|
||||
styles={{
|
||||
root: {
|
||||
selectors: { "::before": { background: "#E1DFDD" } },
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Learn about{" "}
|
||||
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
|
||||
writing effective prompts
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Callout>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack style={{ margin: "8px 0" }}>
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank">
|
||||
Read preview terms
|
||||
</Link>
|
||||
{showErrorMessageBar && (
|
||||
<MessageBar messageBarType={MessageBarType.error}>
|
||||
We ran into an error and were not able to execute query.
|
||||
</MessageBar>
|
||||
)}
|
||||
{showInvalidQueryMessageBar && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.info}
|
||||
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
|
||||
>
|
||||
We were unable to generate a query based upon the prompt provided. Please modify the prompt and try again.
|
||||
For examples of how to write a good prompt, please read
|
||||
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
|
||||
this article.
|
||||
</Link>{" "}
|
||||
Our content guidelines can be found
|
||||
<Link href="https://aka.ms/cdb-query-copilot" target="_blank">
|
||||
here.
|
||||
</Link>
|
||||
</MessageBar>
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{showFeedbackBar && (
|
||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
style={{ padding: 8 }}
|
||||
target="#likeBtn"
|
||||
onDismiss={() => {
|
||||
setShowCallout(false);
|
||||
SubmitFeedback({
|
||||
params: {
|
||||
generatedQuery: generatedQuery,
|
||||
likeQuery: likeQuery,
|
||||
description: "",
|
||||
userPrompt: userPrompt,
|
||||
},
|
||||
explorer: explorer,
|
||||
});
|
||||
}}
|
||||
directionalHint={DirectionalHint.topCenter}
|
||||
>
|
||||
<Text>
|
||||
Thank you. Need to give{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowCallout(false);
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
||||
}}
|
||||
>
|
||||
more feedback?
|
||||
</Link>
|
||||
</Text>
|
||||
</Callout>
|
||||
)}
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 20 }}
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setShowCallout(!likeQuery);
|
||||
setLikeQuery(!likeQuery);
|
||||
if (dislikeQuery) {
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
style={{ margin: "0 10px" }}
|
||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||
onClick={() => {
|
||||
if (!dislikeQuery) {
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
||||
setLikeQuery(false);
|
||||
}
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
setShowCallout(false);
|
||||
}}
|
||||
/>
|
||||
<Separator vertical style={{ color: "#EDEBE9" }} />
|
||||
<CommandBarButton
|
||||
onClick={copyGeneratedCode}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
>
|
||||
Copy query
|
||||
</CommandBarButton>
|
||||
<CommandBarButton
|
||||
onClick={() => {
|
||||
setShowDeletePopup(true);
|
||||
}}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
>
|
||||
Delete query
|
||||
</CommandBarButton>
|
||||
</Stack>
|
||||
)}
|
||||
<WelcomeModal visible={isWelcomModalVisible()} />
|
||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||
{query !== "" && query.trim().length !== 0 && (
|
||||
<DeletePopup
|
||||
showDeletePopup={showDeletePopup}
|
||||
setShowDeletePopup={setShowDeletePopup}
|
||||
setQuery={setQuery}
|
||||
clearFeedback={clearFeedback}
|
||||
showFeedbackBar={setShowFeedbackBar}
|
||||
/>
|
||||
)}
|
||||
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,242 +1,33 @@
|
||||
/* eslint-disable no-console */
|
||||
import {
|
||||
Callout,
|
||||
CommandBarButton,
|
||||
DefaultButton,
|
||||
DirectionalHint,
|
||||
IButtonStyles,
|
||||
IconButton,
|
||||
Image,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Separator,
|
||||
Spinner,
|
||||
Stack,
|
||||
TeachingBubble,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import {
|
||||
ContainerStatusType,
|
||||
PoolIdType,
|
||||
QueryCopilotSampleContainerSchema,
|
||||
ShortenedQueryCopilotSampleContainerSchema,
|
||||
} from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
||||
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
||||
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
||||
import { OnExecuteQueryClick, SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
|
||||
import { userContext } from "UserContext";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import SplitterLayout from "react-splitter-layout";
|
||||
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||
import HintIcon from "../../../images/Hint.svg";
|
||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||
import RecentIcon from "../../../images/Recent.svg";
|
||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
|
||||
interface SuggestedPrompt {
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const promptStyles: IButtonStyles = {
|
||||
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
|
||||
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
||||
};
|
||||
import * as StringUtility from "../../Shared/StringUtility";
|
||||
|
||||
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
||||
const inputEdited = useRef(false);
|
||||
const {
|
||||
hideFeedbackModalForLikedQueries,
|
||||
userPrompt,
|
||||
setUserPrompt,
|
||||
generatedQuery,
|
||||
setGeneratedQuery,
|
||||
query,
|
||||
setQuery,
|
||||
selectedQuery,
|
||||
setSelectedQuery,
|
||||
isGeneratingQuery,
|
||||
setIsGeneratingQuery,
|
||||
likeQuery,
|
||||
setLikeQuery,
|
||||
dislikeQuery,
|
||||
setDislikeQuery,
|
||||
showCallout,
|
||||
setShowCallout,
|
||||
showSamplePrompts,
|
||||
setShowSamplePrompts,
|
||||
isSamplePromptsOpen,
|
||||
setIsSamplePromptsOpen,
|
||||
showDeletePopup,
|
||||
setShowDeletePopup,
|
||||
showFeedbackBar,
|
||||
setShowFeedbackBar,
|
||||
showCopyPopup,
|
||||
setshowCopyPopup,
|
||||
showErrorMessageBar,
|
||||
showInvalidQueryMessageBar,
|
||||
setShowInvalidQueryMessageBar,
|
||||
setShowErrorMessageBar,
|
||||
setGeneratedQueryComments,
|
||||
setQueryResults,
|
||||
setErrorMessage,
|
||||
} = useQueryCopilot();
|
||||
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
||||
|
||||
const sampleProps: SamplePromptsProps = {
|
||||
isSamplePromptsOpen: isSamplePromptsOpen,
|
||||
setIsSamplePromptsOpen: setIsSamplePromptsOpen,
|
||||
setTextBox: setUserPrompt,
|
||||
};
|
||||
|
||||
const copyGeneratedCode = () => {
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
const queryElement = document.createElement("textarea");
|
||||
queryElement.value = query;
|
||||
document.body.appendChild(queryElement);
|
||||
queryElement.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(queryElement);
|
||||
|
||||
setshowCopyPopup(true);
|
||||
setTimeout(() => {
|
||||
setshowCopyPopup(false);
|
||||
}, 6000);
|
||||
};
|
||||
|
||||
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
||||
const cachedHistories = cachedHistoriesString?.split("|");
|
||||
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
||||
const suggestedPrompts: SuggestedPrompt[] = [
|
||||
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
|
||||
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
|
||||
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
|
||||
];
|
||||
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
|
||||
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
|
||||
|
||||
const handleUserPromptChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
inputEdited.current = true;
|
||||
const { value } = event.target;
|
||||
setUserPrompt(value);
|
||||
|
||||
// Filter history prompts
|
||||
const filteredHistory = histories.filter((history) => history.toLowerCase().includes(value.toLowerCase()));
|
||||
setFilteredHistories(filteredHistory);
|
||||
|
||||
// Filter suggested prompts
|
||||
const filteredSuggested = suggestedPrompts.filter((prompt) =>
|
||||
prompt.text.toLowerCase().includes(value.toLowerCase()),
|
||||
);
|
||||
setFilteredSuggestedPrompts(filteredSuggested);
|
||||
};
|
||||
|
||||
const updateHistories = (): void => {
|
||||
const formattedUserPrompt = userPrompt.replace(/\s+/g, " ").trim();
|
||||
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
|
||||
|
||||
const updatedHistories = existingHistories.filter(
|
||||
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase(),
|
||||
);
|
||||
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||
|
||||
setHistories(newHistories);
|
||||
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
|
||||
};
|
||||
|
||||
const resetMessageStates = (): void => {
|
||||
setShowErrorMessageBar(false);
|
||||
setShowInvalidQueryMessageBar(false);
|
||||
setShowFeedbackBar(false);
|
||||
};
|
||||
|
||||
const resetQueryResults = (): void => {
|
||||
setQueryResults(null);
|
||||
setErrorMessage("");
|
||||
};
|
||||
|
||||
const generateSQLQuery = async (): Promise<void> => {
|
||||
try {
|
||||
resetMessageStates();
|
||||
setIsGeneratingQuery(true);
|
||||
setShowDeletePopup(false);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
if (
|
||||
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
|
||||
!userContext.features.disableCopilotPhoenixGateaway
|
||||
) {
|
||||
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
||||
}
|
||||
const payload = {
|
||||
containerSchema: userContext.features.enableCopilotFullSchema
|
||||
? QueryCopilotSampleContainerSchema
|
||||
: ShortenedQueryCopilotSampleContainerSchema,
|
||||
userPrompt: userPrompt,
|
||||
};
|
||||
useQueryCopilot.getState().refreshCorrelationId();
|
||||
const serverInfo = useQueryCopilot.getState().notebookServerInfo;
|
||||
const queryUri = userContext.features.disableCopilotPhoenixGateaway
|
||||
? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery")
|
||||
: createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery");
|
||||
const response = await fetch(queryUri, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||
if (response.ok) {
|
||||
if (generateSQLQueryResponse?.sql !== "N/A") {
|
||||
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
||||
if (generateSQLQueryResponse.explanation) {
|
||||
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
||||
}
|
||||
query += generateSQLQueryResponse.sql;
|
||||
setQuery(query);
|
||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||
setShowFeedbackBar(true);
|
||||
resetQueryResults();
|
||||
} else {
|
||||
setShowInvalidQueryMessageBar(true);
|
||||
}
|
||||
} else {
|
||||
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
|
||||
useTabs.getState().setIsQueryErrorThrown(true);
|
||||
setShowErrorMessageBar(true);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "executeNaturalLanguageQuery");
|
||||
useTabs.getState().setIsQueryErrorThrown(true);
|
||||
setShowErrorMessageBar(true);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsGeneratingQuery(false);
|
||||
useTabs.getState().setIsTabExecuting(false);
|
||||
}
|
||||
};
|
||||
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
||||
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
||||
);
|
||||
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
||||
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
||||
: true;
|
||||
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
|
||||
|
||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||
@@ -261,324 +52,47 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
// Sample Prompts temporary disabled due current design
|
||||
// const samplePromptsBtn = {
|
||||
// iconSrc: SamplePromptsIcon,
|
||||
// iconAlt: "Sample Prompts",
|
||||
// onCommandClick: () => setIsSamplePromptsOpen(true),
|
||||
// commandButtonLabel: "Sample Prompts",
|
||||
// ariaLabel: "Sample Prompts",
|
||||
// hasPopup: false,
|
||||
// };
|
||||
const toggleCopilotButton = {
|
||||
iconSrc: QueryCommandIcon,
|
||||
iconAlt: "Copilot",
|
||||
onCommandClick: () => {
|
||||
toggleCopilot(true);
|
||||
},
|
||||
commandButtonLabel: "Copilot",
|
||||
ariaLabel: "Copilot",
|
||||
hasPopup: false,
|
||||
disabled: copilotActive,
|
||||
};
|
||||
|
||||
return [executeQueryBtn, saveQueryBtn];
|
||||
};
|
||||
const showTeachingBubble = (): void => {
|
||||
if (!inputEdited.current) {
|
||||
setTimeout(() => {
|
||||
if (!inputEdited.current) {
|
||||
toggleCopilotTeachingBubbleVisible();
|
||||
inputEdited.current = true;
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
};
|
||||
|
||||
const clearFeedback = () => {
|
||||
resetButtonState();
|
||||
resetQueryResults();
|
||||
};
|
||||
|
||||
const resetButtonState = () => {
|
||||
setDislikeQuery(false);
|
||||
setLikeQuery(false);
|
||||
setShowCallout(false);
|
||||
};
|
||||
|
||||
const startGenerateQueryProcess = () => {
|
||||
updateHistories();
|
||||
generateSQLQuery();
|
||||
resetButtonState();
|
||||
return [executeQueryBtn, saveQueryBtn, toggleCopilotButton];
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||
}, [query, selectedQuery]);
|
||||
}, [query, selectedQuery, copilotActive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
showTeachingBubble();
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
return () => {
|
||||
const commandbarButtons = getCommandbarButtons();
|
||||
commandbarButtons.pop();
|
||||
commandbarButtons.map((props: CommandButtonComponentProps) => (props.disabled = true));
|
||||
useCommandBar.getState().setContextButtons(commandbarButtons);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleCopilot = (toggle: boolean) => {
|
||||
setCopilotActive(toggle);
|
||||
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, toggle.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="tab-pane" style={{ padding: 24, width: "100%" }}>
|
||||
<Stack className="tab-pane" style={{ width: "100%" }}>
|
||||
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} />
|
||||
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
|
||||
<TextField
|
||||
id="naturalLanguageInput"
|
||||
value={userPrompt}
|
||||
onChange={handleUserPromptChange}
|
||||
onClick={() => {
|
||||
inputEdited.current = true;
|
||||
setShowSamplePrompts(true);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
inputEdited.current = true;
|
||||
startGenerateQueryProcess();
|
||||
}
|
||||
}}
|
||||
style={{ lineHeight: 30 }}
|
||||
styles={{ root: { width: "95%" } }}
|
||||
disabled={isGeneratingQuery}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{copilotTeachingBubbleVisible && (
|
||||
<TeachingBubble
|
||||
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
||||
target="#naturalLanguageInput"
|
||||
hasCloseButton={true}
|
||||
closeButtonAriaLabel="Close"
|
||||
onDismiss={toggleCopilotTeachingBubbleVisible}
|
||||
hasSmallHeadline={true}
|
||||
headline="Write a prompt"
|
||||
>
|
||||
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowSamplePrompts(true);
|
||||
toggleCopilotTeachingBubbleVisible();
|
||||
}}
|
||||
style={{ color: "white", fontWeight: 600 }}
|
||||
>
|
||||
sample prompts
|
||||
</Link>{" "}
|
||||
or write your own query
|
||||
</TeachingBubble>
|
||||
)}
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => startGenerateQueryProcess()}
|
||||
/>
|
||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||
{showSamplePrompts && (
|
||||
<Callout
|
||||
styles={{ root: { minWidth: 400 } }}
|
||||
target="#naturalLanguageInput"
|
||||
isBeakVisible={false}
|
||||
onDismiss={() => setShowSamplePrompts(false)}
|
||||
directionalHintFixed={true}
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
alignTargetEdge={true}
|
||||
gapSpace={4}
|
||||
>
|
||||
<Stack>
|
||||
{filteredHistories?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Recent
|
||||
</Text>
|
||||
{filteredHistories.map((history, i) => (
|
||||
<DefaultButton
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setUserPrompt(history);
|
||||
setShowSamplePrompts(false);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
onRenderIcon={() => <Image src={RecentIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{history}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{filteredSuggestedPrompts?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Suggested Prompts
|
||||
</Text>
|
||||
{filteredSuggestedPrompts.map((prompt) => (
|
||||
<DefaultButton
|
||||
key={prompt.id}
|
||||
onClick={() => {
|
||||
setUserPrompt(prompt.text);
|
||||
setShowSamplePrompts(false);
|
||||
inputEdited.current = true;
|
||||
}}
|
||||
onRenderIcon={() => <Image src={HintIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{prompt.text}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
|
||||
<Stack>
|
||||
<Separator
|
||||
styles={{
|
||||
root: {
|
||||
selectors: { "::before": { background: "#E1DFDD" } },
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Learn about{" "}
|
||||
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
|
||||
writing effective prompts
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Callout>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack style={{ marginTop: 8, marginBottom: 24 }}>
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank">
|
||||
Read preview terms
|
||||
</Link>
|
||||
{showErrorMessageBar && (
|
||||
<MessageBar messageBarType={MessageBarType.error}>
|
||||
We ran into an error and were not able to execute query.
|
||||
</MessageBar>
|
||||
)}
|
||||
{showInvalidQueryMessageBar && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.info}
|
||||
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
|
||||
>
|
||||
We were unable to generate a query based upon the prompt provided. Please modify the prompt and try
|
||||
again. For examples of how to write a good prompt, please read
|
||||
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
|
||||
this article.
|
||||
</Link>{" "}
|
||||
Our content guidelines can be found
|
||||
<Link href="https://aka.ms/cdb-query-copilot" target="_blank">
|
||||
here.
|
||||
</Link>
|
||||
</MessageBar>
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{showFeedbackBar && (
|
||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
style={{ padding: 8 }}
|
||||
target="#likeBtn"
|
||||
onDismiss={() => {
|
||||
setShowCallout(false);
|
||||
SubmitFeedback({
|
||||
params: {
|
||||
generatedQuery: generatedQuery,
|
||||
likeQuery: likeQuery,
|
||||
description: "",
|
||||
userPrompt: userPrompt,
|
||||
},
|
||||
explorer: explorer,
|
||||
});
|
||||
}}
|
||||
directionalHint={DirectionalHint.topCenter}
|
||||
>
|
||||
<Text>
|
||||
Thank you. Need to give{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowCallout(false);
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
||||
}}
|
||||
>
|
||||
more feedback?
|
||||
</Link>
|
||||
</Text>
|
||||
</Callout>
|
||||
)}
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 20 }}
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setShowCallout(!likeQuery);
|
||||
setLikeQuery(!likeQuery);
|
||||
if (dislikeQuery) {
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
style={{ margin: "0 10px" }}
|
||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||
onClick={() => {
|
||||
if (!dislikeQuery) {
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
||||
setLikeQuery(false);
|
||||
}
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
setShowCallout(false);
|
||||
}}
|
||||
/>
|
||||
<Separator vertical style={{ color: "#EDEBE9" }} />
|
||||
<CommandBarButton
|
||||
onClick={copyGeneratedCode}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
>
|
||||
Copy code
|
||||
</CommandBarButton>
|
||||
<CommandBarButton
|
||||
onClick={() => {
|
||||
setShowDeletePopup(true);
|
||||
}}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
>
|
||||
Delete code
|
||||
</CommandBarButton>
|
||||
</Stack>
|
||||
{copilotActive && (
|
||||
<QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar>
|
||||
)}
|
||||
|
||||
<Stack className="tabPaneContentContainer">
|
||||
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
||||
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={query}
|
||||
@@ -592,18 +106,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
<QueryCopilotResults />
|
||||
</SplitterLayout>
|
||||
</Stack>
|
||||
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||
{query !== "" && query.trim().length !== 0 && (
|
||||
<DeletePopup
|
||||
showDeletePopup={showDeletePopup}
|
||||
setShowDeletePopup={setShowDeletePopup}
|
||||
setQuery={setQuery}
|
||||
clearFeedback={clearFeedback}
|
||||
showFeedbackBar={setShowFeedbackBar}
|
||||
/>
|
||||
)}
|
||||
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { userContext } from "UserContext";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import * as StringUtility from "../../../Shared/StringUtility";
|
||||
|
||||
export const SendQueryRequest = async ({
|
||||
userPrompt,
|
||||
@@ -184,15 +185,20 @@ export const QueryDocumentsPerPage = async (
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
});
|
||||
} catch (error) {
|
||||
const isCopilotActive = StringUtility.toBoolean(
|
||||
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`),
|
||||
);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
errorMessage: errorMessage,
|
||||
});
|
||||
useQueryCopilot.getState().setErrorMessage(errorMessage);
|
||||
handleError(errorMessage, "executeQueryCopilotTab");
|
||||
useTabs.getState().setIsQueryErrorThrown(true);
|
||||
useQueryCopilot.getState().setShowErrorMessageBar(true);
|
||||
if (isCopilotActive) {
|
||||
useQueryCopilot.getState().setErrorMessage(errorMessage);
|
||||
useQueryCopilot.getState().setShowErrorMessageBar(true);
|
||||
}
|
||||
} finally {
|
||||
useQueryCopilot.getState().setIsExecuting(false);
|
||||
useTabs.getState().setIsTabExecuting(false);
|
||||
|
||||
@@ -5,7 +5,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||
className="tab-pane"
|
||||
style={
|
||||
Object {
|
||||
"padding": 24,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
@@ -18,103 +17,37 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src={Object {}}
|
||||
style={
|
||||
Object {
|
||||
"height": 24,
|
||||
"width": 24,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"fontWeight": 600,
|
||||
"marginLeft": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
Copilot
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 16,
|
||||
"position": "relative",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
autoComplete="off"
|
||||
disabled={false}
|
||||
id="naturalLanguageInput"
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"lineHeight": 30,
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": "95%",
|
||||
<QueryCopilotPromptbar
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
value=""
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
disabled={true}
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Send",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 8,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 24,
|
||||
"marginTop": 8,
|
||||
},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cdb-copilot-preview-terms"
|
||||
target="_blank"
|
||||
>
|
||||
Read preview terms
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</Stack>
|
||||
toggleCopilot={[Function]}
|
||||
/>
|
||||
<Stack
|
||||
className="tabPaneContentContainer"
|
||||
>
|
||||
@@ -123,10 +56,10 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||
onDragEnd={null}
|
||||
onDragStart={null}
|
||||
onSecondaryPaneSizeChange={null}
|
||||
percentage={false}
|
||||
percentage={true}
|
||||
primaryIndex={0}
|
||||
primaryMinSize={100}
|
||||
secondaryMinSize={200}
|
||||
primaryMinSize={30}
|
||||
secondaryMinSize={70}
|
||||
vertical={true}
|
||||
>
|
||||
<EditorReact
|
||||
@@ -142,20 +75,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||
<QueryCopilotResults />
|
||||
</t>
|
||||
</Stack>
|
||||
<WelcomeModal
|
||||
visible={true}
|
||||
/>
|
||||
<DeletePopup
|
||||
clearFeedback={[Function]}
|
||||
setQuery={[Function]}
|
||||
setShowDeletePopup={[Function]}
|
||||
showDeletePopup={false}
|
||||
showFeedbackBar={[Function]}
|
||||
/>
|
||||
<CopyPopup
|
||||
setShowCopyPopup={[Function]}
|
||||
showCopyPopup={false}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities";
|
||||
import {
|
||||
loadDataCommand,
|
||||
@@ -63,28 +61,16 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
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.
|
||||
This tutorial guides you to create and query distributed tables using a sample dataset.
|
||||
<br />
|
||||
<br />
|
||||
When not in the quick start guide, connecting to Azure Cosmos DB for MongoDB vCore is straightforward
|
||||
using your connection string.
|
||||
To start, input the admin password you used during the cluster creation process into the MongoDB vCore
|
||||
terminal.
|
||||
<br />
|
||||
<br />
|
||||
<Link
|
||||
aria-label="View connection string"
|
||||
href=""
|
||||
onClick={() => sendMessage({ type: MessageTypes.OpenVCoreMongoConnectionStringsBlade })}
|
||||
>
|
||||
View connection string
|
||||
</Link>
|
||||
<br />
|
||||
<br />
|
||||
This string contains placeholders for <user> and <password>. 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 ‘Networking’
|
||||
tab of your database settings, or modify your own network's firewall settings, to successfully
|
||||
connect.
|
||||
Note: If you navigate out of the Quick start blade (MongoDB vCore Shell), the session will be
|
||||
closed and all ongoing commands might be interrupted.
|
||||
</Text>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
@@ -103,6 +89,7 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
||||
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.
|
||||
<br />
|
||||
<br />
|
||||
You'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
|
||||
collections are created when you first reference them in a command, so no explicit creation command is
|
||||
@@ -153,14 +140,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.
|
||||
<br />
|
||||
<br />
|
||||
Let's populate your sampleCollection with data. We'll add 10 documents representing books,
|
||||
each with a title, author, and number of pages, to your sampleCollection in the quickstartDB database.
|
||||
We'll add 10 documents representing books, each with a title, author, and number of pages, to
|
||||
your sampleCollection in the quickstartDB database.
|
||||
</Text>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 200 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
|
||||
>
|
||||
Create distributed table
|
||||
Load data
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
@@ -197,7 +184,7 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
Once you’ve inserted data into your sampleCollection, you can retrieve it using queries. MongoDB
|
||||
Once you've 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
|
||||
results.
|
||||
</Text>
|
||||
@@ -205,7 +192,7 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
||||
style={{ marginTop: 16, width: 110 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(queriesCommand)}
|
||||
>
|
||||
Load data
|
||||
Try query
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
@@ -233,7 +220,7 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Integrations"
|
||||
headerText="Next steps"
|
||||
onRenderItemLink={(props, defaultRenderer) =>
|
||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
|
||||
}
|
||||
@@ -242,46 +229,18 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
||||
>
|
||||
<Stack>
|
||||
<Text>
|
||||
Cosmos DB for MongoDB vCore seamlessly integrates with Azure services. These integrations enable
|
||||
Cosmos DB for MongoDB and its partner products to directly interoperate, ensuring a smooth and unified
|
||||
experience, that just works.
|
||||
<b>Migrate existing data</b>
|
||||
<br />
|
||||
<br />
|
||||
Modernize your data seamlessly from an existing MongoDB cluster, whether it's on-premises or
|
||||
hosted in the cloud, to Azure Cosmos DB for MongoDB vCore.
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://learn.microsoft.com/azure-data-studio/extensions/azure-cosmos-db-mongodb-extension"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</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>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
|
||||
@@ -147,7 +147,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
let properties = "(";
|
||||
let values = "(";
|
||||
for (let property in entity) {
|
||||
if (entity[property]._ === null) {
|
||||
if (entity[property]._ === "" || entity[property]._ === undefined) {
|
||||
continue;
|
||||
}
|
||||
properties = properties.concat(`${property}, `);
|
||||
@@ -208,6 +208,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
!originalDocument[property] ||
|
||||
newEntity[property]._.toString() !== originalDocument[property]._.toString()
|
||||
) {
|
||||
if (newEntity[property]._.toString() === "" || newEntity[property]._ === undefined) {
|
||||
continue;
|
||||
}
|
||||
let propertyQuerySegment = this.isStringType(newEntity[property].$)
|
||||
? `${property} = '${newEntity[property]._}',`
|
||||
: `${property} = ${newEntity[property]._},`;
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<button
|
||||
class="filterbtnstyle queryButton"
|
||||
data-bind="
|
||||
click: refreshDocumentsGrid,
|
||||
click: refreshDocumentsGrid.bind($data, true),
|
||||
enable: applyFilterButton.enabled"
|
||||
aria-label="Apply filter"
|
||||
tabindex="0"
|
||||
@@ -176,7 +176,7 @@
|
||||
<img
|
||||
class="refreshcol"
|
||||
src="/refresh-cosmos.svg"
|
||||
data-bind="click: refreshDocumentsGrid"
|
||||
data-bind="click: refreshDocumentsGrid.bind($data, false)"
|
||||
alt="Refresh documents"
|
||||
tabindex="0"
|
||||
/>
|
||||
@@ -209,7 +209,10 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="loadMore">
|
||||
<a role="button" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0"
|
||||
<a
|
||||
role="button"
|
||||
data-bind="click: loadNextPage.bind($data, false), event: { keypress: onLoadMoreKeyInput }"
|
||||
tabindex="0"
|
||||
>Load more</a
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import { format } from "react-string-format";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import NewDocumentIcon from "../../../images/NewDocument.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import UploadIcon from "../../../images/Upload_16x16.svg";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import {
|
||||
DocumentsGridMetrics,
|
||||
@@ -14,15 +17,15 @@ import {
|
||||
QueryCopilotSampleContainerId,
|
||||
QueryCopilotSampleDatabaseId,
|
||||
} 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 { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||
import { readDocument } from "../../Common/dataAccess/readDocument";
|
||||
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 ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -30,6 +33,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as QueryUtils from "../../Utils/QueryUtils";
|
||||
import { extractPartitionKeyValues } from "../../Utils/QueryUtils";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
import Explorer from "../Explorer";
|
||||
@@ -79,6 +83,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
private _resourceTokenPartitionKey: string;
|
||||
private _isQueryCopilotSampleContainer: boolean;
|
||||
private queryAbortController: AbortController;
|
||||
private cancelQueryTimeoutID: NodeJS.Timeout;
|
||||
|
||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||
super(options);
|
||||
@@ -346,6 +351,22 @@ export default class DocumentsTab extends TabsBase {
|
||||
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> {
|
||||
this.isFilterCreated(true);
|
||||
this.isFilterExpanded(true);
|
||||
@@ -375,15 +396,14 @@ export default class DocumentsTab extends TabsBase {
|
||||
return true;
|
||||
};
|
||||
|
||||
public async refreshDocumentsGrid(): Promise<void> {
|
||||
public async refreshDocumentsGrid(applyFilterButtonPressed?: boolean): Promise<void> {
|
||||
// clear documents grid
|
||||
this.documentIds([]);
|
||||
|
||||
try {
|
||||
// reset iterator
|
||||
this._documentsIterator = this.createIterator();
|
||||
// load documents
|
||||
await this.loadNextPage();
|
||||
await this.autoPopulateContent(applyFilterButtonPressed);
|
||||
// collapse filter
|
||||
this.appliedFilter(this.filterContent());
|
||||
this.isFilterExpanded(false);
|
||||
@@ -406,6 +426,11 @@ export default class DocumentsTab extends TabsBase {
|
||||
this.queryAbortController.abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO Doesn't seem to be used: remove?
|
||||
* @param clickedDocumentId
|
||||
* @returns
|
||||
*/
|
||||
public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> {
|
||||
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
|
||||
return Q();
|
||||
@@ -455,7 +480,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
|
||||
this.selectedDocumentContent.setBaseline(value);
|
||||
this.initialDocumentContent(value);
|
||||
const partitionKeyValueArray = extractPartitionKey(
|
||||
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues(
|
||||
savedDocument,
|
||||
this.partitionKey as PartitionKeyDefinition,
|
||||
);
|
||||
@@ -506,7 +531,10 @@ export default class DocumentsTab extends TabsBase {
|
||||
const selectedDocumentId = this.selectedDocumentId();
|
||||
const documentContent = JSON.parse(this.selectedDocumentContent());
|
||||
|
||||
const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition);
|
||||
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues(
|
||||
documentContent,
|
||||
this.partitionKey as PartitionKeyDefinition,
|
||||
);
|
||||
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
|
||||
|
||||
this.isExecutionError(false);
|
||||
@@ -624,8 +652,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
|
||||
if (!this._documentsIterator) {
|
||||
try {
|
||||
this._documentsIterator = this.createIterator();
|
||||
await this.loadNextPage();
|
||||
await this.autoPopulateContent();
|
||||
} catch (error) {
|
||||
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
@@ -716,9 +743,35 @@ export default class DocumentsTab extends TabsBase {
|
||||
this.initDocumentEditor(documentId, content);
|
||||
}
|
||||
|
||||
public loadNextPage(): Q.Promise<any> {
|
||||
public loadNextPage(applyFilterButtonClicked?: boolean): Q.Promise<any> {
|
||||
this.isExecuting(true);
|
||||
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()
|
||||
.then(
|
||||
(documentsIdsResponse = []) => {
|
||||
@@ -774,7 +827,15 @@ export default class DocumentsTab extends TabsBase {
|
||||
}
|
||||
},
|
||||
)
|
||||
.finally(() => this.isExecuting(false));
|
||||
.finally(() => {
|
||||
this.isExecuting(false);
|
||||
if (applyFilterButtonClicked && this.queryTimeoutEnabled()) {
|
||||
clearTimeout(this.cancelQueryTimeoutID);
|
||||
if (!automaticallyCancelQueryAfterTimeout) {
|
||||
useDialog.getState().closeDialog();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
|
||||
@@ -952,4 +1013,8 @@ export default class DocumentsTab extends TabsBase {
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
};
|
||||
}
|
||||
|
||||
private queryTimeoutEnabled(): boolean {
|
||||
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
||||
import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
||||
import { extractPartitionKeyValues } from "Utils/QueryUtils";
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -88,7 +89,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
)
|
||||
.then(
|
||||
(savedDocument: any) => {
|
||||
let partitionKeyArray = extractPartitionKey(
|
||||
const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues(
|
||||
savedDocument,
|
||||
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
||||
);
|
||||
@@ -150,7 +151,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
|
||||
this.documentIds().forEach((documentId: DocumentId) => {
|
||||
if (documentId.rid === updatedDocument._rid) {
|
||||
const partitionKeyArray = extractPartitionKey(
|
||||
const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues(
|
||||
updatedDocument,
|
||||
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
||||
);
|
||||
@@ -289,7 +290,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
}
|
||||
|
||||
private _hasShardKeySpecified(document: any): boolean {
|
||||
return Boolean(extractPartitionKey(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition));
|
||||
return Boolean(extractPartitionKeyValues(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition));
|
||||
}
|
||||
|
||||
private _getPartitionKeyDefinition(): DataModels.PartitionKey {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React, { Fragment } from "react";
|
||||
import SplitterLayout from "react-splitter-layout";
|
||||
import "react-splitter-layout/lib/index.css";
|
||||
import { format } from "react-string-format";
|
||||
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
@@ -80,6 +84,7 @@ interface IQueryTabStates {
|
||||
isExecuting: boolean;
|
||||
showCopilotSidebar: boolean;
|
||||
queryCopilotGeneratedQuery: string;
|
||||
cancelQueryTimeoutID: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
||||
@@ -107,13 +112,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
isExecuting: false,
|
||||
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
|
||||
queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
|
||||
cancelQueryTimeoutID: undefined,
|
||||
};
|
||||
this.isCloseClicked = false;
|
||||
this.splitterId = this.props.tabId + "_splitter";
|
||||
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
||||
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
||||
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
|
||||
|
||||
this.executeQueryButton = {
|
||||
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
||||
visible: true,
|
||||
@@ -250,6 +255,34 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
this.setState({
|
||||
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());
|
||||
|
||||
try {
|
||||
@@ -273,7 +306,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
this.props.tabsBaseInstance.isExecuting(false);
|
||||
this.setState({
|
||||
isExecuting: false,
|
||||
cancelQueryTimeoutID: undefined,
|
||||
});
|
||||
if (this.queryTimeoutEnabled()) {
|
||||
clearTimeout(this.state.cancelQueryTimeoutID);
|
||||
if (!automaticallyCancelQueryAfterTimeout) {
|
||||
useDialog.getState().closeDialog();
|
||||
}
|
||||
}
|
||||
this.togglesOnFocus();
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
@@ -405,6 +445,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
return this.state.sqlQueryEditorContent;
|
||||
}
|
||||
|
||||
private queryTimeoutEnabled(): boolean {
|
||||
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
|
||||
}
|
||||
|
||||
private unsubscribeCopilotSidebar: () => void;
|
||||
|
||||
componentDidMount(): void {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extractPartitionKey } from "@azure/cosmos";
|
||||
import { extractPartitionKeyValues } from "Utils/QueryUtils";
|
||||
import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { readDocument } from "../../Common/dataAccess/readDocument";
|
||||
@@ -42,7 +42,7 @@ export default class ConflictId {
|
||||
}
|
||||
this.partitionKeyProperty = container && container.partitionKeyProperty;
|
||||
this.partitionKey = container && container.partitionKey;
|
||||
this.partitionKeyValue = extractPartitionKey(this.parsedContent, this.partitionKey as any);
|
||||
this.partitionKeyValue = extractPartitionKeyValues(this.parsedContent, this.partitionKey as any);
|
||||
this.stringPartitionKeyValue = this.getPartitionKeyValueAsString();
|
||||
this.id = ko.observable(data.id);
|
||||
this.isDirty = ko.observable(false);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||
import Explorer from "../Explorer";
|
||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
@@ -22,7 +21,7 @@ export const useDatabaseTreeNodes = (container: Explorer, isNotebookEnabled: boo
|
||||
className: "databaseHeader",
|
||||
children: [],
|
||||
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
||||
contextMenu: undefined, // TODO Disable this for now as the actions don't work. ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
||||
onExpanded: async () => {
|
||||
useSelectedNode.getState().setSelectedNode(database);
|
||||
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
||||
|
||||
@@ -7,9 +7,6 @@ import "../less/hostedexplorer.less";
|
||||
import { AuthType } from "./AuthType";
|
||||
import { DatabaseAccount } from "./Contracts/DataModels";
|
||||
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 { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
|
||||
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
|
||||
@@ -20,6 +17,9 @@ import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
|
||||
import "./Platform/Hosted/ConnectScreen.less";
|
||||
import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils";
|
||||
import "./Shared/appInsights";
|
||||
import { useAADAuth } from "./hooks/useAADAuth";
|
||||
import { useConfig } from "./hooks/useConfig";
|
||||
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
|
||||
|
||||
initializeIcons();
|
||||
|
||||
@@ -51,6 +51,7 @@ const App: React.FunctionComponent = () => {
|
||||
authType: AuthType.AAD,
|
||||
databaseAccount,
|
||||
authorizationToken: armToken,
|
||||
graphAuthorizationToken: graphToken
|
||||
};
|
||||
} else if (authType === AuthType.EncryptedToken) {
|
||||
frameWindow.hostedConfig = {
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface AAD {
|
||||
authType: AuthType.AAD;
|
||||
databaseAccount: DatabaseAccount;
|
||||
authorizationToken: string;
|
||||
graphAuthorizationToken: string;
|
||||
}
|
||||
|
||||
export interface ConnectionString {
|
||||
|
||||
40
src/Main.tsx
40
src/Main.tsx
@@ -1,13 +1,13 @@
|
||||
// CSS Dependencies
|
||||
import { initializeIcons, loadTheme } from "@fluentui/react";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||
import { userContext } from "UserContext";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import React, { useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { userContext } from "UserContext";
|
||||
import "../externals/jquery-ui.min.css";
|
||||
import "../externals/jquery-ui.min.js";
|
||||
import "../externals/jquery-ui.structure.min.css";
|
||||
@@ -16,27 +16,27 @@ import "../externals/jquery.dataTables.min.css";
|
||||
import "../externals/jquery.typeahead.min.css";
|
||||
import "../externals/jquery.typeahead.min.js";
|
||||
// Image Dependencies
|
||||
import { Platform } from "ConfigContext";
|
||||
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 hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||
import "../images/favicon.ico";
|
||||
import "../less/TableStyles/CustomizeColumns.less";
|
||||
import "../less/TableStyles/EntityEditor.less";
|
||||
import "../less/TableStyles/fulldatatables.less";
|
||||
import "../less/TableStyles/queryBuilder.less";
|
||||
import * as StyleConstants from "./Common/StyleConstants";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||
import "../less/documentDB.less";
|
||||
import "../less/forms.less";
|
||||
import "../less/infobox.less";
|
||||
import "../less/menus.less";
|
||||
import "../less/messagebox.less";
|
||||
import "../less/resourceTree.less";
|
||||
import "../less/TableStyles/CustomizeColumns.less";
|
||||
import "../less/TableStyles/EntityEditor.less";
|
||||
import "../less/TableStyles/fulldatatables.less";
|
||||
import "../less/TableStyles/queryBuilder.less";
|
||||
import "../less/tree.less";
|
||||
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
||||
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
||||
import * as StyleConstants from "./Common/StyleConstants";
|
||||
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
||||
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
||||
import { Dialog } from "./Explorer/Controls/Dialog";
|
||||
@@ -55,11 +55,11 @@ import "./Explorer/Panes/PanelComponent.less";
|
||||
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
||||
import "./Explorer/SplashScreen/SplashScreen.less";
|
||||
import { Tabs } from "./Explorer/Tabs/Tabs";
|
||||
import "./Libs/jquery";
|
||||
import "./Shared/appInsights";
|
||||
import { useConfig } from "./hooks/useConfig";
|
||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||
import "./Libs/jquery";
|
||||
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
||||
import "./Shared/appInsights";
|
||||
|
||||
initializeIcons();
|
||||
|
||||
@@ -72,6 +72,7 @@ const App: React.FunctionComponent = () => {
|
||||
const config = useConfig();
|
||||
if (config?.platform === Platform.Fabric) {
|
||||
loadTheme(appThemeFabric);
|
||||
import("../less/documentDBFabric.less");
|
||||
}
|
||||
StyleConstants.updateStyles();
|
||||
const explorer = useKnockoutExplorer(config?.platform);
|
||||
@@ -91,7 +92,6 @@ const App: React.FunctionComponent = () => {
|
||||
|
||||
return (
|
||||
<div className="flexContainer" aria-hidden="false">
|
||||
<LoadFabricOverrides />
|
||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||
<div id="freeTierTeachingBubble"> </div>
|
||||
{/* Main Command Bar - Start */}
|
||||
@@ -141,20 +141,8 @@ const App: React.FunctionComponent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<App />, document.body);
|
||||
|
||||
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 <></>;
|
||||
}
|
||||
}
|
||||
const mainElement = document.getElementById("Main");
|
||||
ReactDOM.render(<App />, mainElement);
|
||||
|
||||
function LoadingExplorer(): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
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 <></>;
|
||||
}
|
||||
53
src/Platform/Fabric/FabricUtil.ts
Normal file
53
src/Platform/Fabric/FabricUtil.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
jest.mock("../../../hooks/useDirectories");
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { extractFeatures } from "Platform/Hosted/extractFeatures";
|
||||
import { updateUserContext, userContext } from "UserContext";
|
||||
import React from "react";
|
||||
import { ConnectExplorer } from "./ConnectExplorer";
|
||||
|
||||
@@ -16,3 +18,24 @@ it("shows the connect form", () => {
|
||||
fireEvent.click(screen.getByText("Connect to your account with connection string"));
|
||||
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 });
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { userContext } from "UserContext";
|
||||
import * as React from "react";
|
||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||
import ErrorImage from "../../../../images/error.svg";
|
||||
@@ -29,6 +30,18 @@ export const fetchEncryptedToken = async (connectionString: string): Promise<str
|
||||
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> = ({
|
||||
setEncryptedToken,
|
||||
login,
|
||||
@@ -37,6 +50,8 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||
setConnectionString,
|
||||
}: Props) => {
|
||||
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
|
||||
const [errorMessage, setErrorMessage] = React.useState("");
|
||||
const enableConnectionStringLogin = !userContext.features.disableConnectionStringLogin;
|
||||
|
||||
return (
|
||||
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
|
||||
@@ -46,11 +61,17 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||
<img src={ConnectImage} alt="Azure Cosmos DB" />
|
||||
</p>
|
||||
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
|
||||
{isFormVisible ? (
|
||||
{isFormVisible && enableConnectionStringLogin ? (
|
||||
<form
|
||||
id="connectWithConnectionString"
|
||||
onSubmit={async (event) => {
|
||||
event.preventDefault();
|
||||
setErrorMessage("");
|
||||
|
||||
if (await isAccountRestrictedForConnectionStringLogin(connectionString)) {
|
||||
setErrorMessage("This account has been blocked from connection-string login.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isResourceTokenConnectionString(connectionString)) {
|
||||
setAuthType(AuthType.ResourceToken);
|
||||
@@ -74,10 +95,12 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||
setConnectionString(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<span className="errorDetailsInfoTooltip" style={{ display: "none" }}>
|
||||
<img className="errorImg" src={ErrorImage} alt="Error notification" />
|
||||
<span className="errorDetails"></span>
|
||||
</span>
|
||||
{errorMessage.length > 0 && (
|
||||
<span className="errorDetailsInfoTooltip">
|
||||
<img className="errorImg" src={ErrorImage} alt="Error notification" />
|
||||
<span className="errorDetails">{errorMessage}</span>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="connectExplorerContent">
|
||||
<input className="filterbtnstyle" type="submit" value="Connect" />
|
||||
@@ -89,9 +112,11 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||
) : (
|
||||
<div id="connectWithAad">
|
||||
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
|
||||
<p className="switchConnectTypeText" onClick={showForm}>
|
||||
Connect to your account with connection string
|
||||
</p>
|
||||
{enableConnectionStringLogin && (
|
||||
<p className="switchConnectTypeText" onClick={showForm}>
|
||||
Connect to your account with connection string
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,7 @@ export type Features = {
|
||||
readonly enableCopilotFullSchema: boolean;
|
||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||
readonly enablePriorityBasedExecution: boolean;
|
||||
readonly disableConnectionStringLogin: boolean;
|
||||
|
||||
// can be set via both flight and feature flag
|
||||
autoscaleDefault: boolean;
|
||||
@@ -114,6 +115,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
|
||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -208,3 +208,9 @@ export class FreeTierLimits {
|
||||
public static RU: number = 1000;
|
||||
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.";
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import * as SessionStorageUtility from "./SessionStorageUtility";
|
||||
export { LocalStorageUtility, SessionStorageUtility };
|
||||
export enum StorageKey {
|
||||
ActualItemPerPage,
|
||||
QueryTimeoutEnabled,
|
||||
QueryTimeout,
|
||||
AutomaticallyCancelQueryAfterTimeout,
|
||||
ContainerPaginationEnabled,
|
||||
CustomItemPerPage,
|
||||
DatabaseAccountId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
|
||||
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -47,6 +48,7 @@ export interface VCoreMongoConnectionParams {
|
||||
}
|
||||
|
||||
interface UserContext {
|
||||
readonly fabricDatabaseConnectionInfo?: FabricDatabaseConnectionInfo;
|
||||
readonly authType?: AuthType;
|
||||
readonly masterKey?: string;
|
||||
readonly subscriptionId?: string;
|
||||
@@ -77,6 +79,7 @@ interface UserContext {
|
||||
collectionCreationDefaults: CollectionCreationDefaults;
|
||||
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
||||
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
||||
readonly accountRestrictedFromUser?: boolean;
|
||||
}
|
||||
|
||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||
@@ -169,3 +172,4 @@ function apiType(account: DatabaseAccount | undefined): ApiType {
|
||||
}
|
||||
|
||||
export { updateUserContext, userContext };
|
||||
|
||||
|
||||
@@ -60,3 +60,27 @@ export function getMsalInstance() {
|
||||
const msalInstance = new msal.PublicClientApplication(config);
|
||||
return msalInstance;
|
||||
}
|
||||
|
||||
export async function isAccountRestrictedFromUser(accountName: string, graphToken: string): Promise<boolean> {
|
||||
const checkUserAccessUrl: string = "https://localhost:12901/api/guest/accountrestrictions/accountrestrictedfromuser";
|
||||
// const authorizationHeader = getAuthorizationHeader();
|
||||
try {
|
||||
const response: Response = await fetch(checkUserAccessUrl, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
accountName
|
||||
}),
|
||||
headers: {
|
||||
// [authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.graphAuthorization]: graphToken,
|
||||
[Constants.HttpHeaders.contentType]: "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
const responseText: string = await response.text();
|
||||
return responseText.toLowerCase() === "true";
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function getPriorityLevel(): PriorityLevel {
|
||||
}
|
||||
}
|
||||
|
||||
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
|
||||
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, undefined, next) => {
|
||||
if (isRelevantRequest(requestContext)) {
|
||||
const priorityLevel: PriorityLevel = getPriorityLevel();
|
||||
requestContext.headers["x-ms-cosmos-priority-level"] = priorityLevel as string;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { PartitionKey, PartitionKeyDefinition, PartitionKeyKind } from "@azure/cosmos";
|
||||
import * as Q from "q";
|
||||
import * as sinon from "sinon";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import * as QueryUtils from "./QueryUtils";
|
||||
import { extractPartitionKeyValues } from "./QueryUtils";
|
||||
|
||||
describe("Query Utils", () => {
|
||||
const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => {
|
||||
@@ -94,4 +96,69 @@ describe("Query Utils", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
|
||||
@@ -82,3 +83,22 @@ export const queryPagesUntilContentPresent = async (
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -8,5 +8,7 @@
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
<body>
|
||||
<div id="Main" style="height: 100%"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
import { FabricMessage } from "Contracts/FabricContract";
|
||||
import { FabricDatabaseConnectionInfo, FabricMessage } from "Contracts/FabricContract";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { fetchEncryptedToken } from "Platform/Hosted/Components/ConnectExplorer";
|
||||
import {
|
||||
handleRequestDatabaseResourceTokensResponse,
|
||||
scheduleRefreshDatabaseResourceToken,
|
||||
} from "Platform/Fabric/FabricUtil";
|
||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||
import { fetchAccessData } from "hooks/usePortalAccessToken";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { AccountKind, Flights } from "../Common/Constants";
|
||||
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
||||
import { sendMessage, sendReadyMessage } from "../Common/MessageHandler";
|
||||
import { handleCachedDataMessage, sendMessage, sendReadyMessage } from "../Common/MessageHandler";
|
||||
import { Platform, configContext, updateConfigContext } from "../ConfigContext";
|
||||
import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
@@ -34,7 +36,7 @@ import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { CollectionCreation } from "../Shared/Constants";
|
||||
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
||||
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
|
||||
import { getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||
import { getAuthorizationHeader, getMsalInstance, isAccountRestrictedFromUser } from "../Utils/AuthorizationUtils";
|
||||
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
|
||||
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
|
||||
@@ -100,70 +102,37 @@ async function configureFabric(): Promise<Explorer> {
|
||||
}
|
||||
|
||||
const data: FabricMessage = event.data?.data;
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case "initialize": {
|
||||
// TODO For now, retrieve info from session storage. Replace with info injected into Data Explorer
|
||||
const connectionString = data.connectionString ?? sessionStorage.getItem("connectionString");
|
||||
if (!connectionString) {
|
||||
console.error("No connection string found in session storage");
|
||||
return undefined;
|
||||
}
|
||||
const encryptedToken = await fetchEncryptedToken(connectionString);
|
||||
// TODO Duplicated from useTokenMetadata
|
||||
const encryptedTokenMetadata = await fetchAccessData(encryptedToken);
|
||||
|
||||
const hostedConfig: EncryptedToken = {
|
||||
authType: AuthType.EncryptedToken,
|
||||
encryptedToken,
|
||||
encryptedTokenMetadata,
|
||||
const fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo = {
|
||||
endpoint: data.message.endpoint,
|
||||
databaseId: data.message.databaseId,
|
||||
resourceTokens: data.message.resourceTokens as { [resourceId: string]: string },
|
||||
resourceTokensTimestamp: data.message.resourceTokensTimestamp,
|
||||
};
|
||||
|
||||
explorer = await configureWithEncryptedToken(hostedConfig);
|
||||
explorer = await createExplorerFabric(fabricDatabaseConnectionInfo);
|
||||
resolve(explorer);
|
||||
|
||||
explorer.refreshAllDatabases().then(() => {
|
||||
openFirstContainer(explorer, fabricDatabaseConnectionInfo.databaseId);
|
||||
});
|
||||
scheduleRefreshDatabaseResourceToken();
|
||||
break;
|
||||
}
|
||||
case "newContainer":
|
||||
explorer.onNewCollectionClicked();
|
||||
break;
|
||||
case "openTab": {
|
||||
// Expand database first
|
||||
const databaseName = sessionStorage.getItem("openDatabaseName") ?? data.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 = 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
case "authorizationToken": {
|
||||
handleCachedDataMessage(data);
|
||||
break;
|
||||
}
|
||||
case "allResourceTokens": {
|
||||
// TODO call handleCachedDataMessage when Fabric echoes message id back
|
||||
handleRequestDatabaseResourceTokensResponse(explorer, data.message as FabricDatabaseConnectionInfo);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -178,6 +147,41 @@ 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> {
|
||||
const win = window as unknown as HostedExplorerChildFrame;
|
||||
let explorer: Explorer;
|
||||
@@ -223,9 +227,11 @@ async function configureHosted(): Promise<Explorer> {
|
||||
|
||||
async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
||||
// TODO: Refactor. updateUserContext needs to be called twice because listKeys below depends on userContext.authorizationToken
|
||||
const accountRestrictedFromUser: boolean = await isAccountRestrictedFromUser(config.databaseAccount.name, config.graphAuthorizationToken);
|
||||
updateUserContext({
|
||||
authType: AuthType.AAD,
|
||||
authorizationToken: `Bearer ${config.authorizationToken}`,
|
||||
accountRestrictedFromUser
|
||||
});
|
||||
const account = config.databaseAccount;
|
||||
const accountResourceId = account.id;
|
||||
@@ -315,6 +321,25 @@ function configureHostedWithResourceToken(config: ResourceToken): 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 {
|
||||
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
|
||||
updateUserContext({
|
||||
|
||||
@@ -128,7 +128,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
||||
|
||||
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
|
||||
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
|
||||
closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }),
|
||||
closeFeedbackModal: () => set({ showFeedbackModal: false }),
|
||||
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
|
||||
set({ hideFeedbackModalForLikedQueries }),
|
||||
refreshCorrelationId: () => set({ correlationId: guid() }),
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
<mimeMap fileExtension="woff" mimeType="application/font-woff" />
|
||||
</staticContent>
|
||||
<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&feature.disableConnectionStringLogin=true" redirectType="Permanent" />
|
||||
</rule>
|
||||
</rules>
|
||||
<outboundRules>
|
||||
<rule name="Strict-Transport-Security" enabled="true">
|
||||
<match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
|
||||
|
||||
Reference in New Issue
Block a user