mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-10 21:19:08 +00:00
Compare commits
3 Commits
users/aisa
...
copilot_re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06973bd3d5 | ||
|
|
44667beba9 | ||
|
|
0566f19e87 |
@@ -1179,16 +1179,16 @@ menuQuickStart {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#tbodycontent tr.gridRowSelected {
|
.gridRowSelected {
|
||||||
.active();
|
.active();
|
||||||
}
|
}
|
||||||
|
|
||||||
#tbodycontent tr.gridRowSelected:hover {
|
.gridRowSelected:hover {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
.hover();
|
.hover();
|
||||||
}
|
}
|
||||||
|
|
||||||
#tbodycontent tr.gridRowHighlighted {
|
.gridRowHighlighted {
|
||||||
border-style: dotted;
|
border-style: dotted;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
@@ -2576,10 +2576,9 @@ a:link {
|
|||||||
.querydropdown.placeholderVisible {
|
.querydropdown.placeholderVisible {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.querydropdown.placeholderVisible::placeholder {
|
.querydropdown.placeholderVisible::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||||
/* Chrome, Firefox, Opera, Safari 10.1+ */
|
|
||||||
color: #767474;
|
color: #767474;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.querydropdown:hover {
|
.querydropdown:hover {
|
||||||
@@ -2649,7 +2648,7 @@ a:link {
|
|||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
border-bottom: 2px solid rgba(0, 120, 212, 1);
|
border-bottom: 2px solid rgba(0,120,212,1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active:focus > .tabNavContentContainer {
|
.nav-tabs > li.active:focus > .tabNavContentContainer {
|
||||||
@@ -3097,3 +3096,4 @@ a:link {
|
|||||||
background: white;
|
background: white;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -179,11 +179,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@azure/cosmos": {
|
"@azure/cosmos": {
|
||||||
"version": "4.0.0",
|
"version": "3.16.2",
|
||||||
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/@azure/cosmos/-/cosmos-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.16.2.tgz",
|
||||||
"integrity": "sha1-X9qLNctiu82lIVm5bEw5gahD1bk=",
|
"integrity": "sha512-sceY5LWj0BHGj8PSyaVCfDRQLVZyoCfIY78kyIROJVEw0k+p9XFs8fhpykN8JklkCftL0WlaVY+X25SQwnhZsw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@azure/abort-controller": "^1.0.0",
|
|
||||||
"@azure/core-auth": "^1.3.0",
|
"@azure/core-auth": "^1.3.0",
|
||||||
"@azure/core-rest-pipeline": "^1.2.0",
|
"@azure/core-rest-pipeline": "^1.2.0",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
@@ -22134,12 +22133,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz",
|
||||||
"integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA=="
|
"integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA=="
|
||||||
},
|
},
|
||||||
"react-string-format": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/react-string-format/-/react-string-format-1.0.1.tgz",
|
|
||||||
"integrity": "sha1-JyQaRZHqURInBBx64HC3FJBh3AA=",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"react-syntax-highlighter": {
|
"react-syntax-highlighter": {
|
||||||
"version": "12.2.1",
|
"version": "12.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "4.0.0",
|
"@azure/cosmos": "3.16.2",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "1.2.1",
|
"@azure/identity": "1.2.1",
|
||||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
"@azure/ms-rest-nodeauth": "3.0.7",
|
||||||
@@ -92,7 +92,6 @@
|
|||||||
"react-notification-system": "0.2.17",
|
"react-notification-system": "0.2.17",
|
||||||
"react-redux": "7.1.3",
|
"react-redux": "7.1.3",
|
||||||
"react-splitter-layout": "4.0.0",
|
"react-splitter-layout": "4.0.0",
|
||||||
"react-string-format": "1.0.1",
|
|
||||||
"react-youtube": "9.0.1",
|
"react-youtube": "9.0.1",
|
||||||
"redux": "4.0.4",
|
"redux": "4.0.4",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
@@ -235,4 +234,4 @@
|
|||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +177,6 @@ export class HttpHeaders {
|
|||||||
public static activityId: string = "x-ms-activity-id";
|
public static activityId: string = "x-ms-activity-id";
|
||||||
public static apiType: string = "x-ms-cosmos-apitype";
|
public static apiType: string = "x-ms-cosmos-apitype";
|
||||||
public static authorization: string = "authorization";
|
public static authorization: string = "authorization";
|
||||||
public static graphAuthorization: string = "graph-authorization";
|
|
||||||
public static collectionIndexTransformationProgress: string =
|
public static collectionIndexTransformationProgress: string =
|
||||||
"x-ms-documentdb-collection-index-transformation-progress";
|
"x-ms-documentdb-collection-index-transformation-progress";
|
||||||
public static continuation: string = "x-ms-continuation";
|
public static continuation: string = "x-ms-continuation";
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ describe("requestPlugin", () => {
|
|||||||
const headers = {};
|
const headers = {};
|
||||||
const endpoint = "https://docs.azure.com";
|
const endpoint = "https://docs.azure.com";
|
||||||
const path = "/dbs/foo";
|
const path = "/dbs/foo";
|
||||||
requestPlugin({ endpoint, headers, path } as any, undefined, next as any);
|
requestPlugin({ endpoint, headers, path } as any, next as any);
|
||||||
expect(next.mock.calls[0][0]).toMatchSnapshot();
|
expect(next.mock.calls[0][0]).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -137,7 +137,7 @@ describe("requestPlugin", () => {
|
|||||||
const headers = {};
|
const headers = {};
|
||||||
const endpoint = "";
|
const endpoint = "";
|
||||||
const path = "/dbs/foo";
|
const path = "/dbs/foo";
|
||||||
requestPlugin({ endpoint, headers, path } as any, undefined, next as any);
|
requestPlugin({ endpoint, headers, path } as any, next as any);
|
||||||
expect(next.mock.calls[0][0]).toMatchSnapshot();
|
expect(next.mock.calls[0][0]).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
import { configContext, Platform } from "../ConfigContext";
|
||||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
|
||||||
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
|
|
||||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../AuthType";
|
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
import { getErrorMessage } from "./ErrorHandlingUtils";
|
import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { PriorityLevel } from "../Common/Constants";
|
||||||
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
|
import { AuthType } from "../AuthType";
|
||||||
|
|
||||||
const _global = typeof self === "undefined" ? window : self;
|
const _global = typeof self === "undefined" ? window : self;
|
||||||
|
|
||||||
@@ -29,36 +26,6 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configContext.platform === Platform.Fabric) {
|
|
||||||
switch (requestInfo.resourceType) {
|
|
||||||
case Cosmos.ResourceType.conflicts:
|
|
||||||
case Cosmos.ResourceType.container:
|
|
||||||
case Cosmos.ResourceType.sproc:
|
|
||||||
case Cosmos.ResourceType.udf:
|
|
||||||
case Cosmos.ResourceType.trigger:
|
|
||||||
case Cosmos.ResourceType.item:
|
|
||||||
case Cosmos.ResourceType.pkranges:
|
|
||||||
// User resource tokens
|
|
||||||
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
|
||||||
const resourceTokens = userContext.fabricDatabaseConnectionInfo.resourceTokens;
|
|
||||||
checkDatabaseResourceTokensValidity(userContext.fabricDatabaseConnectionInfo.resourceTokensTimestamp);
|
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
|
||||||
|
|
||||||
case Cosmos.ResourceType.none:
|
|
||||||
case Cosmos.ResourceType.database:
|
|
||||||
case Cosmos.ResourceType.offer:
|
|
||||||
case Cosmos.ResourceType.user:
|
|
||||||
case Cosmos.ResourceType.permission:
|
|
||||||
// User master tokens
|
|
||||||
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [
|
|
||||||
requestInfo,
|
|
||||||
]);
|
|
||||||
console.log("Response from Fabric: ", authorizationToken);
|
|
||||||
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
|
||||||
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userContext.masterKey) {
|
if (userContext.masterKey) {
|
||||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||||
@@ -74,7 +41,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
return decodeURIComponent(result.PrimaryReadWriteToken);
|
return decodeURIComponent(result.PrimaryReadWriteToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, diagnosticNode, next) => {
|
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
|
||||||
requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href;
|
requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href;
|
||||||
requestContext.headers["x-ms-proxy-target"] = endpoint();
|
requestContext.headers["x-ms-proxy-target"] = endpoint();
|
||||||
return next(requestContext);
|
return next(requestContext);
|
||||||
@@ -89,11 +56,7 @@ export const endpoint = () => {
|
|||||||
return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
|
return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getTokenFromAuthService(
|
export async function getTokenFromAuthService(verb: string, resourceType: string, resourceId?: string): Promise<any> {
|
||||||
verb: string,
|
|
||||||
resourceType: string,
|
|
||||||
resourceId?: string,
|
|
||||||
): Promise<AuthorizationToken> {
|
|
||||||
try {
|
try {
|
||||||
const host = configContext.BACKEND_ENDPOINT;
|
const host = configContext.BACKEND_ENDPOINT;
|
||||||
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function handleCachedDataMessage(message: any): void {
|
|||||||
if (messageContent.error != null) {
|
if (messageContent.error != null) {
|
||||||
cachedDataPromise.deferred.reject(messageContent.error);
|
cachedDataPromise.deferred.reject(messageContent.error);
|
||||||
} else {
|
} else {
|
||||||
cachedDataPromise.deferred.resolve(messageContent.data);
|
cachedDataPromise.deferred.resolve(JSON.parse(messageContent.data));
|
||||||
}
|
}
|
||||||
runGarbageCollector();
|
runGarbageCollector();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,18 @@
|
|||||||
import { ContainerResponse } from "@azure/cosmos";
|
|
||||||
import { Queries } from "Common/Constants";
|
import { Queries } from "Common/Constants";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||||
import { listSqlContainers } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
import { listSqlContainers } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
import { listTables } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
import { listTables } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||||
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||||
|
|
||||||
if (
|
|
||||||
configContext.platform === Platform.Fabric &&
|
|
||||||
userContext.fabricDatabaseConnectionInfo &&
|
|
||||||
userContext.fabricDatabaseConnectionInfo.databaseId === databaseId
|
|
||||||
) {
|
|
||||||
const collections: DataModels.Collection[] = [];
|
|
||||||
const promises: Promise<ContainerResponse>[] = [];
|
|
||||||
|
|
||||||
for (const collectionResourceId in userContext.fabricDatabaseConnectionInfo.resourceTokens) {
|
|
||||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
|
||||||
const resourceIdObj = collectionResourceId.split("/");
|
|
||||||
const tokenDatabaseId = resourceIdObj[1];
|
|
||||||
const tokenCollectionId = resourceIdObj[3];
|
|
||||||
|
|
||||||
if (tokenDatabaseId === databaseId) {
|
|
||||||
promises.push(client().database(databaseId).container(tokenCollectionId).read());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import { Platform, configContext } from "ConfigContext";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
|
|
||||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||||
if (configContext.platform === Platform.Fabric) {
|
|
||||||
// TODO This works, but is very slow, because it requests the token, so we skip for now
|
|
||||||
console.error("Skiping readDatabaseOffer for Fabric");
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,45 +1,17 @@
|
|||||||
import { Platform, configContext } from "ConfigContext";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||||
import { listSqlDatabases } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
import { listSqlDatabases } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
export async function readDatabases(): Promise<DataModels.Database[]> {
|
export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||||
let databases: DataModels.Database[];
|
let databases: DataModels.Database[];
|
||||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||||
|
|
||||||
if (configContext.platform === Platform.Fabric && userContext.fabricDatabaseConnectionInfo?.resourceTokens) {
|
|
||||||
const tokensData = userContext.fabricDatabaseConnectionInfo;
|
|
||||||
|
|
||||||
const databaseIdsSet = new Set<string>(); // databaseId
|
|
||||||
|
|
||||||
for (const collectionResourceId in tokensData.resourceTokens) {
|
|
||||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
|
||||||
const resourceIdObj = collectionResourceId.split("/");
|
|
||||||
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 {
|
try {
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
export function getAuthorizationTokenUsingResourceTokens(
|
|
||||||
resourceTokens: { [resourceId: string]: string },
|
|
||||||
path: string,
|
|
||||||
resourceId: string,
|
|
||||||
): string {
|
|
||||||
// console.log(`getting token for path: "${path}" and resourceId: "${resourceId}"`);
|
|
||||||
|
|
||||||
if (resourceTokens && Object.keys(resourceTokens).length > 0) {
|
|
||||||
// For database account access(through getDatabaseAccount API), path and resourceId are "",
|
|
||||||
// so in this case we return the first token to be used for creating the auth header as the
|
|
||||||
// service will accept any token in this case
|
|
||||||
if (!path && !resourceId) {
|
|
||||||
return resourceTokens[Object.keys(resourceTokens)[0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have exact resource token for the path use it
|
|
||||||
if (resourceId && resourceTokens[resourceId]) {
|
|
||||||
return resourceTokens[resourceId];
|
|
||||||
}
|
|
||||||
|
|
||||||
// minimum valid path /dbs
|
|
||||||
if (!path || path.length < 4) {
|
|
||||||
console.error(
|
|
||||||
`Unable to get authotization token for Path:"${path}" and resourcerId:"${resourceId}". Invalid path.`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
path = trimSlashFromLeftAndRight(path);
|
|
||||||
const pathSegments = (path && path.split("/")) || [];
|
|
||||||
|
|
||||||
// Item path
|
|
||||||
if (pathSegments.length === 6) {
|
|
||||||
// Look for a container token matching the item path
|
|
||||||
const containerPath = pathSegments.slice(0, 4).map(decodeURIComponent).join("/");
|
|
||||||
if (resourceTokens[containerPath]) {
|
|
||||||
return resourceTokens[containerPath];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is legacy behavior that lets someone use a resource token pointing ONLY at an ID
|
|
||||||
// It was used when _rid was exposed by the SDK, but now that we are using user provided ids it is not needed
|
|
||||||
// However removing it now would be a breaking change
|
|
||||||
// if it's an incomplete path like /dbs/db1/colls/, start from the parent resource
|
|
||||||
let index = pathSegments.length % 2 === 0 ? pathSegments.length - 1 : pathSegments.length - 2;
|
|
||||||
for (; index > 0; index -= 2) {
|
|
||||||
const id = decodeURI(pathSegments[index]);
|
|
||||||
if (resourceTokens[id]) {
|
|
||||||
return resourceTokens[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`Unable to get authotization token for Path:"${path}" and resourcerId:"${resourceId}"`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimLeftSlashes = new RegExp("^[/]+");
|
|
||||||
const trimRightSlashes = new RegExp("[/]+$");
|
|
||||||
function trimSlashFromLeftAndRight(inputString: string): string {
|
|
||||||
if (typeof inputString !== "string") {
|
|
||||||
throw new Error("invalid input: input is not string");
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputString.replace(trimLeftSlashes, "").replace(trimRightSlashes, "");
|
|
||||||
}
|
|
||||||
@@ -64,7 +64,6 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
||||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
|
|||||||
@@ -1,6 +1,46 @@
|
|||||||
import { MessageTypes } from "Contracts/MessageTypes";
|
|
||||||
import * as ActionContracts from "./ActionContracts";
|
import * as ActionContracts from "./ActionContracts";
|
||||||
import * as Diagnostics from "./Diagnostics";
|
import * as Diagnostics from "./Diagnostics";
|
||||||
import * as Versions from "./Versions";
|
import * as Versions from "./Versions";
|
||||||
|
|
||||||
export { ActionContracts, Diagnostics, MessageTypes, Versions };
|
/**
|
||||||
|
* Messaging types used with Data Explorer <-> Portal communication
|
||||||
|
* and Hosted <-> Explorer communication
|
||||||
|
*/
|
||||||
|
export enum MessageTypes {
|
||||||
|
TelemetryInfo,
|
||||||
|
LogInfo,
|
||||||
|
RefreshResources,
|
||||||
|
AllDatabases,
|
||||||
|
CollectionsForDatabase,
|
||||||
|
RefreshOffers,
|
||||||
|
AllOffers,
|
||||||
|
UpdateLocationHash,
|
||||||
|
SingleOffer,
|
||||||
|
RefreshOffer,
|
||||||
|
UpdateAccountName,
|
||||||
|
ForbiddenError,
|
||||||
|
AadSignIn,
|
||||||
|
GetAccessAadRequest,
|
||||||
|
GetAccessAadResponse,
|
||||||
|
UpdateAccountSwitch,
|
||||||
|
UpdateDirectoryControl,
|
||||||
|
SwitchAccount,
|
||||||
|
SendNotification,
|
||||||
|
ClearNotification,
|
||||||
|
ExplorerClickEvent,
|
||||||
|
LoadingStatus,
|
||||||
|
GetArcadiaToken,
|
||||||
|
CreateWorkspace,
|
||||||
|
CreateSparkPool,
|
||||||
|
RefreshDatabaseAccount,
|
||||||
|
CloseTab,
|
||||||
|
OpenQuickstartBlade,
|
||||||
|
OpenPostgreSQLPasswordReset,
|
||||||
|
OpenPostgresNetworkingBlade,
|
||||||
|
OpenCosmosDBNetworkingBlade,
|
||||||
|
DisplayNPSSurvey,
|
||||||
|
OpenVCoreMongoNetworkingBlade,
|
||||||
|
OpenVCoreMongoConnectionStringsBlade,
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ActionContracts, Diagnostics, Versions };
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { AuthorizationToken, MessageTypes } from "./MessageTypes";
|
|
||||||
|
|
||||||
export type FabricMessage =
|
export type FabricMessage =
|
||||||
| {
|
| {
|
||||||
type: "newContainer";
|
type: "newContainer";
|
||||||
@@ -7,71 +5,21 @@ export type FabricMessage =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "initialize";
|
type: "initialize";
|
||||||
message: {
|
connectionString: string | undefined;
|
||||||
endpoint: string | undefined;
|
|
||||||
databaseId: string | undefined;
|
|
||||||
resourceTokens: unknown | undefined;
|
|
||||||
resourceTokensTimestamp: number | undefined;
|
|
||||||
error: string | undefined;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "authorizationToken";
|
type: "openTab";
|
||||||
message: {
|
databaseName: string;
|
||||||
id: string;
|
collectionName: string | undefined;
|
||||||
error: string | undefined;
|
|
||||||
data: AuthorizationToken | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "allResourceTokens";
|
|
||||||
message: {
|
|
||||||
endpoint: string | undefined;
|
|
||||||
databaseId: string | undefined;
|
|
||||||
resourceTokens: unknown | undefined;
|
|
||||||
resourceTokensTimestamp: number | undefined;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataExploreMessage =
|
export type DataExploreMessage =
|
||||||
| "ready"
|
| "ready"
|
||||||
| {
|
| {
|
||||||
type: MessageTypes.TelemetryInfo;
|
type: number;
|
||||||
data: {
|
data: {
|
||||||
action: "LoadDatabases";
|
action: "LoadDatabases";
|
||||||
actionModifier: "success" | "start";
|
actionModifier: "success" | "start";
|
||||||
defaultExperience: "SQL";
|
defaultExperience: "SQL";
|
||||||
};
|
};
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: MessageTypes.GetAuthorizationToken;
|
|
||||||
id: string;
|
|
||||||
params: GetCosmosTokenMessageOptions[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: MessageTypes.GetAllResourceTokens;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetCosmosTokenMessageOptions = {
|
|
||||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
|
||||||
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
|
|
||||||
resourceId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CosmosDBTokenResponse = {
|
|
||||||
token: string;
|
|
||||||
date: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CosmosDBConnectionInfoResponse = {
|
|
||||||
endpoint: string;
|
|
||||||
databaseId: string;
|
|
||||||
resourceTokens: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface FabricDatabaseConnectionInfo {
|
|
||||||
endpoint: string;
|
|
||||||
databaseId: string;
|
|
||||||
resourceTokens: { [resourceId: string]: string };
|
|
||||||
resourceTokensTimestamp: number;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Messaging types used with Data Explorer <-> Portal communication,
|
|
||||||
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
|
|
||||||
*/
|
|
||||||
export enum MessageTypes {
|
|
||||||
TelemetryInfo,
|
|
||||||
LogInfo,
|
|
||||||
RefreshResources,
|
|
||||||
AllDatabases,
|
|
||||||
CollectionsForDatabase,
|
|
||||||
RefreshOffers,
|
|
||||||
AllOffers,
|
|
||||||
UpdateLocationHash,
|
|
||||||
SingleOffer,
|
|
||||||
RefreshOffer,
|
|
||||||
UpdateAccountName,
|
|
||||||
ForbiddenError,
|
|
||||||
AadSignIn,
|
|
||||||
GetAccessAadRequest,
|
|
||||||
GetAccessAadResponse,
|
|
||||||
UpdateAccountSwitch,
|
|
||||||
UpdateDirectoryControl,
|
|
||||||
SwitchAccount,
|
|
||||||
SendNotification,
|
|
||||||
ClearNotification,
|
|
||||||
ExplorerClickEvent,
|
|
||||||
LoadingStatus,
|
|
||||||
GetArcadiaToken,
|
|
||||||
CreateWorkspace,
|
|
||||||
CreateSparkPool,
|
|
||||||
RefreshDatabaseAccount,
|
|
||||||
CloseTab,
|
|
||||||
OpenQuickstartBlade,
|
|
||||||
OpenPostgreSQLPasswordReset,
|
|
||||||
OpenPostgresNetworkingBlade,
|
|
||||||
OpenCosmosDBNetworkingBlade,
|
|
||||||
DisplayNPSSurvey,
|
|
||||||
OpenVCoreMongoNetworkingBlade,
|
|
||||||
OpenVCoreMongoConnectionStringsBlade,
|
|
||||||
|
|
||||||
// Data Explorer -> Fabric communication
|
|
||||||
GetAuthorizationToken,
|
|
||||||
GetAllResourceTokens,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthorizationToken {
|
|
||||||
XDate: string;
|
|
||||||
PrimaryReadWriteToken: string;
|
|
||||||
}
|
|
||||||
4
src/Definitions/less.d.ts
vendored
4
src/Definitions/less.d.ts
vendored
@@ -1,4 +0,0 @@
|
|||||||
declare module "*.less" {
|
|
||||||
const value: string;
|
|
||||||
export default value;
|
|
||||||
}
|
|
||||||
@@ -129,22 +129,20 @@ export const createCollectionContextMenuButton = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configContext.platform !== Platform.Fabric) {
|
items.push({
|
||||||
items.push({
|
iconSrc: DeleteCollectionIcon,
|
||||||
iconSrc: DeleteCollectionIcon,
|
onClick: () => {
|
||||||
onClick: () => {
|
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
useSidePanel
|
||||||
useSidePanel
|
.getState()
|
||||||
.getState()
|
.openSidePanel(
|
||||||
.openSidePanel(
|
"Delete " + getCollectionName(),
|
||||||
"Delete " + getCollectionName(),
|
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
);
|
||||||
);
|
},
|
||||||
},
|
label: `Delete ${getCollectionName()}`,
|
||||||
label: `Delete ${getCollectionName()}`,
|
styleClass: "deleteCollectionMenuItem",
|
||||||
styleClass: "deleteCollectionMenuItem",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -123,6 +123,19 @@ describe("ContainerSampleGenerator", () => {
|
|||||||
await generator.createSampleContainerAsync();
|
await generator.createSampleContainerAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not create any sample for Mongo API account", async () => {
|
||||||
|
const experience = "Sample generation not supported for this API Mongo";
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
properties: {
|
||||||
|
capabilities: [{ name: "EnableMongo" }],
|
||||||
|
},
|
||||||
|
} as DatabaseAccount,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
||||||
|
});
|
||||||
|
|
||||||
it("should not create any sample for Table API account", async () => {
|
it("should not create any sample for Table API account", async () => {
|
||||||
const experience = "Sample generation not supported for this API Tables";
|
const experience = "Sample generation not supported for this API Tables";
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { sendMessage } from "Common/MessageHandler";
|
|||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
@@ -380,13 +379,6 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onRefreshResourcesClick = (): void => {
|
public onRefreshResourcesClick = (): void => {
|
||||||
if (configContext.platform === Platform.Fabric) {
|
|
||||||
// Requesting the tokens will trigger a refresh of the databases
|
|
||||||
// TODO: Once the id is returned from Fabric, we can await this call and then refresh the databases here
|
|
||||||
requestDatabaseResourceTokens();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: this.refreshAllDatabases();
|
||||||
|
|||||||
@@ -50,36 +50,31 @@ export function createStaticCommandBarButtons(
|
|||||||
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newCollectionBtn = createNewCollectionGroup(container);
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
// Avoid starting with a divider
|
buttons.push(newCollectionBtn);
|
||||||
const addDivider = () => {
|
if (
|
||||||
if (buttons.length > 0) {
|
configContext.platform !== Platform.Fabric &&
|
||||||
|
userContext.apiType !== "Tables" &&
|
||||||
|
userContext.apiType !== "Cassandra"
|
||||||
|
) {
|
||||||
|
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||||
|
|
||||||
|
if (addSynapseLink) {
|
||||||
buttons.push(createDivider());
|
buttons.push(createDivider());
|
||||||
}
|
buttons.push(addSynapseLink);
|
||||||
};
|
|
||||||
|
|
||||||
if (configContext.platform !== Platform.Fabric) {
|
|
||||||
const newCollectionBtn = createNewCollectionGroup(container);
|
|
||||||
buttons.push(newCollectionBtn);
|
|
||||||
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
|
||||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
|
||||||
|
|
||||||
if (addSynapseLink) {
|
|
||||||
addDivider();
|
|
||||||
buttons.push(addSynapseLink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userContext.apiType !== "Tables") {
|
|
||||||
newCollectionBtn.children = [createNewCollectionGroup(container)];
|
|
||||||
const newDatabaseBtn = createNewDatabase(container);
|
|
||||||
newCollectionBtn.children.push(newDatabaseBtn);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userContext.apiType !== "Tables") {
|
||||||
|
newCollectionBtn.children = [createNewCollectionGroup(container)];
|
||||||
|
const newDatabaseBtn = createNewDatabase(container);
|
||||||
|
newCollectionBtn.children.push(newDatabaseBtn);
|
||||||
|
}
|
||||||
|
|
||||||
if (useNotebook.getState().isNotebookEnabled) {
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
addDivider();
|
buttons.push(createDivider());
|
||||||
const notebookButtons: CommandButtonComponentProps[] = [];
|
const notebookButtons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
const newNotebookButton = createNewNotebookButton(container);
|
const newNotebookButton = createNewNotebookButton(container);
|
||||||
@@ -133,7 +128,7 @@ export function createStaticCommandBarButtons(
|
|||||||
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||||
|
|
||||||
if (isQuerySupported) {
|
if (isQuerySupported) {
|
||||||
addDivider();
|
buttons.push(createDivider());
|
||||||
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
|
||||||
buttons.push(newSqlQueryBtn);
|
buttons.push(newSqlQueryBtn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,8 +114,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
createNewDatabase:
|
createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
|
||||||
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
|
|
||||||
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
||||||
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
||||||
selectedDatabaseId:
|
selectedDatabaseId:
|
||||||
@@ -275,38 +274,36 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{configContext.platform !== Platform.Fabric && (
|
<Stack horizontal verticalAlign="center">
|
||||||
<Stack horizontal verticalAlign="center">
|
<div role="radiogroup">
|
||||||
<div role="radiogroup">
|
<input
|
||||||
<input
|
className="panelRadioBtn"
|
||||||
className="panelRadioBtn"
|
checked={this.state.createNewDatabase}
|
||||||
checked={this.state.createNewDatabase}
|
aria-label="Create new database"
|
||||||
aria-label="Create new database"
|
aria-checked={this.state.createNewDatabase}
|
||||||
aria-checked={this.state.createNewDatabase}
|
name="databaseType"
|
||||||
name="databaseType"
|
type="radio"
|
||||||
type="radio"
|
role="radio"
|
||||||
role="radio"
|
id="databaseCreateNew"
|
||||||
id="databaseCreateNew"
|
tabIndex={0}
|
||||||
tabIndex={0}
|
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
/>
|
||||||
/>
|
<span className="panelRadioBtnLabel">Create new</span>
|
||||||
<span className="panelRadioBtnLabel">Create new</span>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={!this.state.createNewDatabase}
|
checked={!this.state.createNewDatabase}
|
||||||
aria-label="Use existing database"
|
aria-label="Use existing database"
|
||||||
aria-checked={!this.state.createNewDatabase}
|
aria-checked={!this.state.createNewDatabase}
|
||||||
name="databaseType"
|
name="databaseType"
|
||||||
type="radio"
|
type="radio"
|
||||||
role="radio"
|
role="radio"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||||
/>
|
/>
|
||||||
<span className="panelRadioBtnLabel">Use existing</span>
|
<span className="panelRadioBtnLabel">Use existing</span>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
|
||||||
|
|
||||||
{this.state.createNewDatabase && (
|
{this.state.createNewDatabase && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
|
|||||||
@@ -32,8 +32,14 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
|||||||
return (
|
return (
|
||||||
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
||||||
{icon}
|
{icon}
|
||||||
<span className="panelWarningErrorDetailsLinkContainer" role="alert" aria-live="assertive">
|
<span className="panelWarningErrorDetailsLinkContainer">
|
||||||
<Text aria-label={message} className="panelWarningErrorMessage" variant="small">
|
<Text
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
aria-label={message}
|
||||||
|
className="panelWarningErrorMessage"
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
{message}
|
{message}
|
||||||
{link && linkText && (
|
{link && linkText && (
|
||||||
<Link target="_blank" href={link}>
|
<Link target="_blank" href={link}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import { ReactWrapper, mount } from "enzyme";
|
import { mount, ReactWrapper } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { RightPaneForm } from "./RightPaneForm";
|
import { RightPaneForm } from "./RightPaneForm";
|
||||||
|
|
||||||
@@ -34,6 +34,6 @@ describe("Right Pane Form", () => {
|
|||||||
it("should render error in header", () => {
|
it("should render error in header", () => {
|
||||||
render(<RightPaneForm {...props} formError="file already Exist" />);
|
render(<RightPaneForm {...props} formError="file already Exist" />);
|
||||||
expect(screen.getByLabelText("error")).toBeDefined();
|
expect(screen.getByLabelText("error")).toBeDefined();
|
||||||
expect(screen.getByRole("alert").innerHTML).toContain("file already Exist");
|
expect(screen.getByRole("alert").innerHTML).toEqual("file already Exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
import {
|
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
|
||||||
Checkbox,
|
|
||||||
ChoiceGroup,
|
|
||||||
IChoiceGroupOption,
|
|
||||||
ISpinButtonStyles,
|
|
||||||
IToggleStyles,
|
|
||||||
Position,
|
|
||||||
SpinButton,
|
|
||||||
Toggle,
|
|
||||||
} from "@fluentui/react";
|
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
import { configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
@@ -15,10 +6,10 @@ import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
|||||||
import * as StringUtility from "Shared/StringUtility";
|
import * as StringUtility from "Shared/StringUtility";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, MouseEvent, useState } from "react";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
|
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||||
|
|
||||||
export const SettingsPane: FunctionComponent = () => {
|
export const SettingsPane: FunctionComponent = () => {
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
@@ -28,13 +19,6 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
? Constants.Queries.UnlimitedPageOption
|
? Constants.Queries.UnlimitedPageOption
|
||||||
: Constants.Queries.CustomPageOption,
|
: Constants.Queries.CustomPageOption,
|
||||||
);
|
);
|
||||||
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
|
|
||||||
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
|
|
||||||
);
|
|
||||||
const [queryTimeout, setQueryTimeout] = useState<number>(LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout));
|
|
||||||
const [automaticallyCancelQueryAfterTimeout, setAutomaticallyCancelQueryAfterTimeout] = useState<boolean>(
|
|
||||||
LocalStorageUtility.getEntryBoolean(StorageKey.AutomaticallyCancelQueryAfterTimeout),
|
|
||||||
);
|
|
||||||
const [customItemPerPage, setCustomItemPerPage] = useState<number>(
|
const [customItemPerPage, setCustomItemPerPage] = useState<number>(
|
||||||
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0,
|
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0,
|
||||||
);
|
);
|
||||||
@@ -69,7 +53,7 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
||||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
||||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
||||||
const handlerOnSubmit = () => {
|
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
LocalStorageUtility.setEntryNumber(
|
LocalStorageUtility.setEntryNumber(
|
||||||
@@ -77,7 +61,6 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
||||||
);
|
);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||||
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
|
||||||
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
|
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
|
||||||
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
||||||
@@ -90,14 +73,6 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryTimeoutEnabled) {
|
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout);
|
|
||||||
LocalStorageUtility.setEntryBoolean(
|
|
||||||
StorageKey.AutomaticallyCancelQueryAfterTimeout,
|
|
||||||
automaticallyCancelQueryAfterTimeout,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsExecuting(false);
|
setIsExecuting(false);
|
||||||
logConsoleInfo(
|
logConsoleInfo(
|
||||||
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
|
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
|
||||||
@@ -122,6 +97,7 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
||||||
);
|
);
|
||||||
closeSidePanel();
|
closeSidePanel();
|
||||||
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCustomPageOptionSelected = () => {
|
const isCustomPageOptionSelected = () => {
|
||||||
@@ -136,7 +112,7 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
formError: "",
|
formError: "",
|
||||||
isExecuting,
|
isExecuting,
|
||||||
submitButtonText: "Apply",
|
submitButtonText: "Apply",
|
||||||
onSubmit: () => handlerOnSubmit(),
|
onSubmit: () => handlerOnSubmit(undefined),
|
||||||
};
|
};
|
||||||
const pageOptionList: IChoiceGroupOption[] = [
|
const pageOptionList: IChoiceGroupOption[] = [
|
||||||
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
|
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
|
||||||
@@ -164,21 +140,6 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
setPageOption(option.key);
|
setPageOption(option.key);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
|
||||||
setQueryTimeoutEnabled(checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnAutomaticallyCancelQueryToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
|
||||||
setAutomaticallyCancelQueryAfterTimeout(checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnQueryTimeoutSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
|
|
||||||
const queryTimeout = Number(newValue);
|
|
||||||
if (!isNaN(queryTimeout)) {
|
|
||||||
setQueryTimeout(queryTimeout);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const choiceButtonStyles = {
|
const choiceButtonStyles = {
|
||||||
root: {
|
root: {
|
||||||
clear: "both",
|
clear: "both",
|
||||||
@@ -200,35 +161,6 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryTimeoutToggleStyles: IToggleStyles = {
|
|
||||||
label: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 400,
|
|
||||||
display: "block",
|
|
||||||
},
|
|
||||||
root: {},
|
|
||||||
container: {},
|
|
||||||
pill: {},
|
|
||||||
thumb: {},
|
|
||||||
text: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryTimeoutSpinButtonStyles: ISpinButtonStyles = {
|
|
||||||
label: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 400,
|
|
||||||
},
|
|
||||||
root: {
|
|
||||||
paddingBottom: 10,
|
|
||||||
},
|
|
||||||
labelWrapper: {},
|
|
||||||
icon: {},
|
|
||||||
spinButtonWrapper: {},
|
|
||||||
input: {},
|
|
||||||
arrowButtonsContainer: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...genericPaneProps}>
|
<RightPaneForm {...genericPaneProps}>
|
||||||
<div className="paneMainContent">
|
<div className="paneMainContent">
|
||||||
@@ -279,50 +211,6 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{userContext.apiType === "SQL" && (
|
|
||||||
<div className="settingsSection">
|
|
||||||
<div className="settingsSectionPart">
|
|
||||||
<div>
|
|
||||||
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
|
|
||||||
Query Timeout
|
|
||||||
</legend>
|
|
||||||
<InfoTooltip>
|
|
||||||
When a query reaches a specified time limit, a popup with an option to cancel the query will show
|
|
||||||
unless automatic cancellation has been enabled
|
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Toggle
|
|
||||||
styles={queryTimeoutToggleStyles}
|
|
||||||
label="Enable query timeout"
|
|
||||||
onChange={handleOnQueryTimeoutToggleChange}
|
|
||||||
defaultChecked={queryTimeoutEnabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{queryTimeoutEnabled && (
|
|
||||||
<div>
|
|
||||||
<SpinButton
|
|
||||||
label="Query timeout (ms)"
|
|
||||||
labelPosition={Position.top}
|
|
||||||
defaultValue={(queryTimeout || 5000).toString()}
|
|
||||||
min={100}
|
|
||||||
step={1000}
|
|
||||||
onChange={handleOnQueryTimeoutSpinButtonChange}
|
|
||||||
incrementButtonAriaLabel="Increase value by 1000"
|
|
||||||
decrementButtonAriaLabel="Decrease value by 1000"
|
|
||||||
styles={queryTimeoutSpinButtonStyles}
|
|
||||||
/>
|
|
||||||
<Toggle
|
|
||||||
label="Automatically cancel query after timeout"
|
|
||||||
styles={queryTimeoutToggleStyles}
|
|
||||||
onChange={handleOnAutomaticallyCancelQueryToggleChange}
|
|
||||||
defaultChecked={automaticallyCancelQueryAfterTimeout}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart">
|
<div className="settingsSectionPart">
|
||||||
<div className="settingsSectionLabel">
|
<div className="settingsSectionLabel">
|
||||||
|
|||||||
@@ -97,46 +97,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="settingsSection"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="settingsSectionPart"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<legend
|
|
||||||
className="settingsSectionLabel legendLabel"
|
|
||||||
id="queryTimeoutLabel"
|
|
||||||
>
|
|
||||||
Query Timeout
|
|
||||||
</legend>
|
|
||||||
<InfoTooltip>
|
|
||||||
When a query reaches a specified time limit, a popup with an option to cancel the query will show unless automatic cancellation has been enabled
|
|
||||||
</InfoTooltip>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<StyledToggleBase
|
|
||||||
defaultChecked={false}
|
|
||||||
label="Enable query timeout"
|
|
||||||
onChange={[Function]}
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"container": Object {},
|
|
||||||
"label": Object {
|
|
||||||
"display": "block",
|
|
||||||
"fontSize": 12,
|
|
||||||
"fontWeight": 400,
|
|
||||||
},
|
|
||||||
"pill": Object {},
|
|
||||||
"root": Object {},
|
|
||||||
"text": Object {},
|
|
||||||
"thumb": Object {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -390,9 +390,6 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="panelNullWarning" style={{ padding: "20px", color: "red" }}>
|
|
||||||
Warning: Null fields will not be displayed for editing.
|
|
||||||
</div>
|
|
||||||
</RightPaneForm>
|
</RightPaneForm>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -355,17 +355,6 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="panelNullWarning"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"color": "red",
|
|
||||||
"padding": "20px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Warning: Null fields will not be displayed for editing.
|
|
||||||
</div>
|
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="Update"
|
buttonLabel="Update"
|
||||||
isButtonDisabled={false}
|
isButtonDisabled={false}
|
||||||
|
|||||||
@@ -323,19 +323,21 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
</IconBase>
|
</IconBase>
|
||||||
</StyledIconBase>
|
</StyledIconBase>
|
||||||
<span
|
<span
|
||||||
aria-live="assertive"
|
|
||||||
className="panelWarningErrorDetailsLinkContainer"
|
className="panelWarningErrorDetailsLinkContainer"
|
||||||
key=".0:$.1"
|
key=".0:$.1"
|
||||||
role="alert"
|
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
|
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
|
||||||
|
aria-live="assertive"
|
||||||
className="panelWarningErrorMessage"
|
className="panelWarningErrorMessage"
|
||||||
|
role="alert"
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
|
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
|
||||||
|
aria-live="assertive"
|
||||||
className="panelWarningErrorMessage css-56"
|
className="panelWarningErrorMessage css-56"
|
||||||
|
role="alert"
|
||||||
>
|
>
|
||||||
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
|
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not submit submission if required description field is null", () => {
|
it("should submit submission", () => {
|
||||||
const explorer = new Explorer();
|
const explorer = new Explorer();
|
||||||
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
|
||||||
|
|
||||||
@@ -110,24 +110,12 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
|||||||
submitButton.simulate("click");
|
submitButton.simulate("click");
|
||||||
wrapper.setProps({});
|
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).toHaveBeenCalledTimes(1);
|
||||||
expect(SubmitFeedback).toHaveBeenCalledWith({
|
expect(SubmitFeedback).toHaveBeenCalledWith({
|
||||||
params: {
|
params: {
|
||||||
likeQuery: false,
|
likeQuery: false,
|
||||||
generatedQuery: "test query",
|
generatedQuery: "",
|
||||||
userPrompt: "test prompt",
|
userPrompt: "",
|
||||||
description: "",
|
description: "",
|
||||||
contact: getUserEmail(),
|
contact: getUserEmail(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,94 +25,93 @@ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }):
|
|||||||
closeFeedbackModal,
|
closeFeedbackModal,
|
||||||
setHideFeedbackModalForLikedQueries,
|
setHideFeedbackModalForLikedQueries,
|
||||||
} = useQueryCopilot();
|
} = useQueryCopilot();
|
||||||
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(false);
|
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(true);
|
||||||
const [description, setDescription] = React.useState<string>("");
|
const [description, setDescription] = React.useState<string>("");
|
||||||
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
||||||
const [contact, setContact] = React.useState<string>(getUserEmail());
|
const [contact, setContact] = React.useState<string>(getUserEmail());
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
closeFeedbackModal();
|
|
||||||
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
|
||||||
SubmitFeedback({
|
|
||||||
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
|
||||||
explorer: explorer,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={showFeedbackModal}>
|
<Modal isOpen={showFeedbackModal}>
|
||||||
<form onSubmit={handleSubmit}>
|
<Stack style={{ padding: 24 }}>
|
||||||
<Stack style={{ padding: 24 }}>
|
<Stack horizontal horizontalAlign="space-between">
|
||||||
<Stack horizontal horizontalAlign="space-between">
|
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
|
||||||
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
|
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
|
||||||
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
|
|
||||||
</Stack>
|
|
||||||
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
|
|
||||||
<TextField
|
|
||||||
styles={{ root: { marginBottom: 14 } }}
|
|
||||||
label="Description"
|
|
||||||
required
|
|
||||||
placeholder="Provide more details"
|
|
||||||
value={description}
|
|
||||||
onChange={(_, newValue) => setDescription(newValue)}
|
|
||||||
multiline
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
styles={{ root: { marginBottom: 14 } }}
|
|
||||||
label="Query generated"
|
|
||||||
defaultValue={generatedQuery}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<ChoiceGroup
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
marginBottom: 14,
|
|
||||||
},
|
|
||||||
flexContainer: {
|
|
||||||
selectors: {
|
|
||||||
".ms-ChoiceField-field::before": { marginTop: 4 },
|
|
||||||
".ms-ChoiceField-field::after": { marginTop: 4 },
|
|
||||||
".ms-ChoiceFieldLabel": { paddingLeft: 6 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
label="May we contact you about your feedback?"
|
|
||||||
options={[
|
|
||||||
{ key: "yes", text: "Yes, you may contact me." },
|
|
||||||
{ key: "no", text: "No, do not contact me." },
|
|
||||||
]}
|
|
||||||
selectedKey={isContactAllowed ? "yes" : "no"}
|
|
||||||
onChange={(_, option) => {
|
|
||||||
setIsContactAllowed(option.key === "yes");
|
|
||||||
setContact(option.key === "yes" ? getUserEmail() : "");
|
|
||||||
}}
|
|
||||||
></ChoiceGroup>
|
|
||||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
|
|
||||||
{
|
|
||||||
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
|
|
||||||
Privacy statement
|
|
||||||
</Link>
|
|
||||||
}{" "}
|
|
||||||
for more information.
|
|
||||||
</Text>
|
|
||||||
{likeQuery && (
|
|
||||||
<Checkbox
|
|
||||||
styles={{ label: { paddingLeft: 0 }, root: { marginBottom: 14 } }}
|
|
||||||
label="Don't show me this next time"
|
|
||||||
checked={doNotShowAgainChecked}
|
|
||||||
onChange={(_, checked) => setDoNotShowAgainChecked(checked)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Stack horizontal horizontalAlign="end">
|
|
||||||
<PrimaryButton styles={{ root: { marginRight: 8 } }} type="submit">
|
|
||||||
Submit
|
|
||||||
</PrimaryButton>
|
|
||||||
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
|
||||||
|
<TextField
|
||||||
|
styles={{ root: { marginBottom: 14 } }}
|
||||||
|
label="Description"
|
||||||
|
required
|
||||||
|
placeholder="Provide more details"
|
||||||
|
value={description}
|
||||||
|
onChange={(_, newValue) => setDescription(newValue)}
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
styles={{ root: { marginBottom: 14 } }}
|
||||||
|
label="Query generated"
|
||||||
|
defaultValue={generatedQuery}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<ChoiceGroup
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
marginBottom: 14,
|
||||||
|
},
|
||||||
|
flexContainer: {
|
||||||
|
selectors: {
|
||||||
|
".ms-ChoiceField-field::before": { marginTop: 4 },
|
||||||
|
".ms-ChoiceField-field::after": { marginTop: 4 },
|
||||||
|
".ms-ChoiceFieldLabel": { paddingLeft: 6 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
label="May we contact you about your feedback?"
|
||||||
|
options={[
|
||||||
|
{ key: "yes", text: "Yes, you may contact me." },
|
||||||
|
{ key: "no", text: "No, do not contact me." },
|
||||||
|
]}
|
||||||
|
selectedKey={isContactAllowed ? "yes" : "no"}
|
||||||
|
onChange={(_, option) => {
|
||||||
|
setIsContactAllowed(option.key === "yes");
|
||||||
|
setContact(option.key === "yes" ? getUserEmail() : "");
|
||||||
|
}}
|
||||||
|
></ChoiceGroup>
|
||||||
|
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||||
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
|
||||||
|
{
|
||||||
|
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
|
||||||
|
Privacy statement
|
||||||
|
</Link>
|
||||||
|
}{" "}
|
||||||
|
for more information.
|
||||||
|
</Text>
|
||||||
|
{likeQuery && (
|
||||||
|
<Checkbox
|
||||||
|
styles={{ label: { paddingLeft: 0 }, root: { marginBottom: 14 } }}
|
||||||
|
label="Don't show me this next time"
|
||||||
|
checked={doNotShowAgainChecked}
|
||||||
|
onChange={(_, checked) => setDoNotShowAgainChecked(checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Stack horizontal horizontalAlign="end">
|
||||||
|
<PrimaryButton
|
||||||
|
styles={{ root: { marginRight: 8 } }}
|
||||||
|
onClick={() => {
|
||||||
|
closeFeedbackModal();
|
||||||
|
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
||||||
|
SubmitFeedback({
|
||||||
|
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
||||||
|
explorer: explorer,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</PrimaryButton>
|
||||||
|
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
import {
|
import {
|
||||||
Callout,
|
Callout,
|
||||||
CommandBarButton,
|
CommandBarButton,
|
||||||
@@ -140,7 +141,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
|
|
||||||
// Filter suggested prompts
|
// Filter suggested prompts
|
||||||
const filteredSuggested = suggestedPrompts.filter((prompt) =>
|
const filteredSuggested = suggestedPrompts.filter((prompt) =>
|
||||||
prompt.text.toLowerCase().includes(value.toLowerCase()),
|
prompt.text.toLowerCase().includes(value.toLowerCase())
|
||||||
);
|
);
|
||||||
setFilteredSuggestedPrompts(filteredSuggested);
|
setFilteredSuggestedPrompts(filteredSuggested);
|
||||||
};
|
};
|
||||||
@@ -150,7 +151,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
|
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
|
||||||
|
|
||||||
const updatedHistories = existingHistories.filter(
|
const updatedHistories = existingHistories.filter(
|
||||||
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase(),
|
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase()
|
||||||
);
|
);
|
||||||
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||||
|
|
||||||
@@ -237,7 +238,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
const showTeachingBubble = (): void => {
|
const showTeachingBubble = (): void => {
|
||||||
if (!inputEdited.current) {
|
if (!inputEdited.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!inputEdited.current && !isWelcomModalVisible()) {
|
if (!inputEdited.current) {
|
||||||
toggleCopilotTeachingBubbleVisible();
|
toggleCopilotTeachingBubbleVisible();
|
||||||
inputEdited.current = true;
|
inputEdited.current = true;
|
||||||
}
|
}
|
||||||
@@ -245,10 +246,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isWelcomModalVisible = (): boolean => {
|
|
||||||
return localStorage.getItem("hideWelcomeModal") !== "true";
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearFeedback = () => {
|
const clearFeedback = () => {
|
||||||
resetButtonState();
|
resetButtonState();
|
||||||
resetQueryResults();
|
resetQueryResults();
|
||||||
@@ -301,7 +298,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
setShowSamplePrompts(true);
|
setShowSamplePrompts(true);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" && userPrompt) {
|
if (e.key === "Enter") {
|
||||||
inputEdited.current = true;
|
inputEdited.current = true;
|
||||||
startGenerateQueryProcess();
|
startGenerateQueryProcess();
|
||||||
}
|
}
|
||||||
@@ -537,7 +534,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
iconProps={{ iconName: "Copy" }}
|
iconProps={{ iconName: "Copy" }}
|
||||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||||
>
|
>
|
||||||
Copy query
|
Copy code
|
||||||
</CommandBarButton>
|
</CommandBarButton>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -546,11 +543,11 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
iconProps={{ iconName: "Delete" }}
|
iconProps={{ iconName: "Delete" }}
|
||||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||||
>
|
>
|
||||||
Delete query
|
Delete code
|
||||||
</CommandBarButton>
|
</CommandBarButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<WelcomeModal visible={isWelcomModalVisible()} />
|
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
||||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||||
{query !== "" && query.trim().length !== 0 && (
|
{query !== "" && query.trim().length !== 0 && (
|
||||||
<DeletePopup
|
<DeletePopup
|
||||||
|
|||||||
@@ -21,13 +21,8 @@ import * as StringUtility from "../../Shared/StringUtility";
|
|||||||
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
|
||||||
|
|
||||||
const cachedCopilotToggleStatus: string = localStorage.getItem(
|
const cachedCopilotToggleStatus = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`);
|
||||||
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
|
const [copilotActive, setCopilotActive] = useState<boolean>(StringUtility.toBoolean(cachedCopilotToggleStatus));
|
||||||
);
|
|
||||||
const copilotInitialActive: boolean = cachedCopilotToggleStatus
|
|
||||||
? StringUtility.toBoolean(cachedCopilotToggleStatus)
|
|
||||||
: true;
|
|
||||||
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
|
|
||||||
|
|
||||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||||
@@ -92,7 +87,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
<QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar>
|
<QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar>
|
||||||
)}
|
)}
|
||||||
<Stack className="tabPaneContentContainer">
|
<Stack className="tabPaneContentContainer">
|
||||||
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
language={"sql"}
|
language={"sql"}
|
||||||
content={query}
|
content={query}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export const OnExecuteQueryClick = async (): Promise<void> => {
|
|||||||
|
|
||||||
export const QueryDocumentsPerPage = async (
|
export const QueryDocumentsPerPage = async (
|
||||||
firstItemIndex: number,
|
firstItemIndex: number,
|
||||||
queryIterator: MinimalQueryIterator,
|
queryIterator: MinimalQueryIterator
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
useQueryCopilot.getState().setIsExecuting(true);
|
useQueryCopilot.getState().setIsExecuting(true);
|
||||||
@@ -174,8 +174,7 @@ export const QueryDocumentsPerPage = async (
|
|||||||
useTabs.getState().setIsQueryErrorThrown(false);
|
useTabs.getState().setIsQueryErrorThrown(false);
|
||||||
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
||||||
firstItemIndex,
|
firstItemIndex,
|
||||||
async (firstItemIndex: number) =>
|
async (firstItemIndex: number) => queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
|
||||||
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useQueryCopilot.getState().setQueryResults(queryResults);
|
useQueryCopilot.getState().setQueryResults(queryResults);
|
||||||
@@ -186,7 +185,7 @@ export const QueryDocumentsPerPage = async (
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isCopilotActive = StringUtility.toBoolean(
|
const isCopilotActive = StringUtility.toBoolean(
|
||||||
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`),
|
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`)
|
||||||
);
|
);
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
|
|||||||
@@ -17,37 +17,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleCopilot={[Function]}
|
|
||||||
/>
|
|
||||||
<Stack
|
<Stack
|
||||||
className="tabPaneContentContainer"
|
className="tabPaneContentContainer"
|
||||||
>
|
>
|
||||||
@@ -56,10 +25,10 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
onDragEnd={null}
|
onDragEnd={null}
|
||||||
onDragStart={null}
|
onDragStart={null}
|
||||||
onSecondaryPaneSizeChange={null}
|
onSecondaryPaneSizeChange={null}
|
||||||
percentage={true}
|
percentage={false}
|
||||||
primaryIndex={0}
|
primaryIndex={0}
|
||||||
primaryMinSize={30}
|
primaryMinSize={100}
|
||||||
secondaryMinSize={70}
|
secondaryMinSize={200}
|
||||||
vertical={true}
|
vertical={true}
|
||||||
>
|
>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities";
|
import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities";
|
||||||
import {
|
import {
|
||||||
loadDataCommand,
|
loadDataCommand,
|
||||||
@@ -61,16 +63,28 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
>
|
>
|
||||||
<Stack style={{ marginTop: 20 }}>
|
<Stack style={{ marginTop: 20 }}>
|
||||||
<Text>
|
<Text>
|
||||||
This tutorial guides you to create and query distributed tables using a sample dataset.
|
A hosted mongosh (mongo shell) is provided for this quick start. You are automatically logged in to
|
||||||
|
mongosh, allowing you to interact with your database directly.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
To start, input the admin password you used during the cluster creation process into the MongoDB vCore
|
When not in the quick start guide, connecting to Azure Cosmos DB for MongoDB vCore is straightforward
|
||||||
terminal.
|
using your connection string.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
<Link
|
||||||
|
aria-label="View connection string"
|
||||||
|
href=""
|
||||||
|
onClick={() => sendMessage({ type: MessageTypes.OpenVCoreMongoConnectionStringsBlade })}
|
||||||
|
>
|
||||||
|
View connection string
|
||||||
|
</Link>
|
||||||
<br />
|
<br />
|
||||||
Note: If you navigate out of the Quick start blade (MongoDB vCore Shell), the session will be
|
<br />
|
||||||
closed and all ongoing commands might be interrupted.
|
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.
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
@@ -89,7 +103,6 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
documents are similar to the columns in a relational database table. One key advantage of MongoDB is
|
documents are similar to the columns in a relational database table. One key advantage of MongoDB is
|
||||||
that these documents within a collection can have different fields.
|
that these documents within a collection can have different fields.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
|
||||||
You're now going to create a new database and a collection within that database using the Mongo
|
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
|
shell. In MongoDB, creating a database or a collection is implicit. This means that databases and
|
||||||
collections are created when you first reference them in a command, so no explicit creation command is
|
collections are created when you first reference them in a command, so no explicit creation command is
|
||||||
@@ -140,14 +153,14 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
with data. In MongoDB, data is stored as documents, which are structured as field and value pairs.
|
with data. In MongoDB, data is stored as documents, which are structured as field and value pairs.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
We'll add 10 documents representing books, each with a title, author, and number of pages, to
|
Let's populate your sampleCollection with data. We'll add 10 documents representing books,
|
||||||
your sampleCollection in the quickstartDB database.
|
each with a title, author, and number of pages, to your sampleCollection in the quickstartDB database.
|
||||||
</Text>
|
</Text>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
style={{ marginTop: 16, width: 200 }}
|
style={{ marginTop: 16, width: 200 }}
|
||||||
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
|
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
|
||||||
>
|
>
|
||||||
Load data
|
Create distributed table
|
||||||
</DefaultButton>
|
</DefaultButton>
|
||||||
<Stack horizontal style={{ marginTop: 16 }}>
|
<Stack horizontal style={{ marginTop: 16 }}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -184,7 +197,7 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
>
|
>
|
||||||
<Stack style={{ marginTop: 20 }}>
|
<Stack style={{ marginTop: 20 }}>
|
||||||
<Text>
|
<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
|
queries can be as simple or as complex as you need them to be, allowing you to filter, sort, and limit
|
||||||
results.
|
results.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -192,7 +205,7 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
style={{ marginTop: 16, width: 110 }}
|
style={{ marginTop: 16, width: 110 }}
|
||||||
onClick={() => useTerminal.getState().sendMessage(queriesCommand)}
|
onClick={() => useTerminal.getState().sendMessage(queriesCommand)}
|
||||||
>
|
>
|
||||||
Try query
|
Load data
|
||||||
</DefaultButton>
|
</DefaultButton>
|
||||||
<Stack horizontal style={{ marginTop: 16 }}>
|
<Stack horizontal style={{ marginTop: 16 }}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -220,7 +233,7 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
headerText="Next steps"
|
headerText="Integrations"
|
||||||
onRenderItemLink={(props, defaultRenderer) =>
|
onRenderItemLink={(props, defaultRenderer) =>
|
||||||
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
|
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
|
||||||
}
|
}
|
||||||
@@ -229,18 +242,46 @@ export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => {
|
|||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>
|
<Text>
|
||||||
<b>Migrate existing data</b>
|
Cosmos DB for MongoDB vCore seamlessly integrates with Azure services. These integrations enable
|
||||||
<br />
|
Cosmos DB for MongoDB and its partner products to directly interoperate, ensuring a smooth and unified
|
||||||
<br />
|
experience, that just works.
|
||||||
Modernize your data seamlessly from an existing MongoDB cluster, whether it'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>
|
</Text>
|
||||||
|
<Stack horizontal>
|
||||||
|
<Stack style={{ marginTop: 20, marginRight: 20 }}>
|
||||||
|
<Text>
|
||||||
|
<b>First party integrations</b>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<b>Azure Monitor</b>
|
||||||
|
<br />
|
||||||
|
Azure monitor provides comprehensive monitoring and diagnostics for Cosmos DB for Mongo DB. Learn
|
||||||
|
more
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<b>Azure Networking</b>
|
||||||
|
<br />
|
||||||
|
Azure Networking seamlessly integrates with Azure Cosmos DB for Mongo DB for fast and secure data
|
||||||
|
access. Learn more
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<b>PowerShell/CLI/ARM</b>
|
||||||
|
<br />
|
||||||
|
PowerShell/CLI/ARM seamlessly integrates with Azure Cosmos DB for Mongo DB for efficient
|
||||||
|
management and automation. Learn more
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack style={{ marginTop: 20, marginLeft: 20 }}>
|
||||||
|
<Text>
|
||||||
|
<b>Application platforms integrations</b>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<b>Vercel</b>
|
||||||
|
<br />
|
||||||
|
Vercel is a cloud platform for hosting static front ends and serverless functions, with instant
|
||||||
|
deployments, automated scaling, and Next.js integration. Learn more
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
</Pivot>
|
</Pivot>
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
let properties = "(";
|
let properties = "(";
|
||||||
let values = "(";
|
let values = "(";
|
||||||
for (let property in entity) {
|
for (let property in entity) {
|
||||||
if (entity[property]._ === "" || entity[property]._ === undefined) {
|
if (entity[property]._ === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
properties = properties.concat(`${property}, `);
|
properties = properties.concat(`${property}, `);
|
||||||
@@ -208,9 +208,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
!originalDocument[property] ||
|
!originalDocument[property] ||
|
||||||
newEntity[property]._.toString() !== originalDocument[property]._.toString()
|
newEntity[property]._.toString() !== originalDocument[property]._.toString()
|
||||||
) {
|
) {
|
||||||
if (newEntity[property]._.toString() === "" || newEntity[property]._ === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let propertyQuerySegment = this.isStringType(newEntity[property].$)
|
let propertyQuerySegment = this.isStringType(newEntity[property].$)
|
||||||
? `${property} = '${newEntity[property]._}',`
|
? `${property} = '${newEntity[property]._}',`
|
||||||
: `${property} = ${newEntity[property]._},`;
|
: `${property} = ${newEntity[property]._},`;
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
<button
|
<button
|
||||||
class="filterbtnstyle queryButton"
|
class="filterbtnstyle queryButton"
|
||||||
data-bind="
|
data-bind="
|
||||||
click: refreshDocumentsGrid.bind($data, true),
|
click: refreshDocumentsGrid,
|
||||||
enable: applyFilterButton.enabled"
|
enable: applyFilterButton.enabled"
|
||||||
aria-label="Apply filter"
|
aria-label="Apply filter"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
<img
|
<img
|
||||||
class="refreshcol"
|
class="refreshcol"
|
||||||
src="/refresh-cosmos.svg"
|
src="/refresh-cosmos.svg"
|
||||||
data-bind="click: refreshDocumentsGrid.bind($data, false)"
|
data-bind="click: refreshDocumentsGrid"
|
||||||
alt="Refresh documents"
|
alt="Refresh documents"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
/>
|
/>
|
||||||
@@ -209,10 +209,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="loadMore">
|
<div class="loadMore">
|
||||||
<a
|
<a role="button" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0"
|
||||||
role="button"
|
|
||||||
data-bind="click: loadNextPage.bind($data, false), event: { keypress: onLoadMoreKeyInput }"
|
|
||||||
tabindex="0"
|
|
||||||
>Load more</a
|
>Load more</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
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 DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
||||||
import NewDocumentIcon from "../../../images/NewDocument.svg";
|
|
||||||
import UploadIcon from "../../../images/Upload_16x16.svg";
|
|
||||||
import DiscardIcon from "../../../images/discard.svg";
|
import DiscardIcon from "../../../images/discard.svg";
|
||||||
|
import NewDocumentIcon from "../../../images/NewDocument.svg";
|
||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
|
import UploadIcon from "../../../images/Upload_16x16.svg";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import {
|
import {
|
||||||
DocumentsGridMetrics,
|
DocumentsGridMetrics,
|
||||||
@@ -17,15 +14,15 @@ import {
|
|||||||
QueryCopilotSampleContainerId,
|
QueryCopilotSampleContainerId,
|
||||||
QueryCopilotSampleDatabaseId,
|
QueryCopilotSampleDatabaseId,
|
||||||
} from "../../Common/Constants";
|
} from "../../Common/Constants";
|
||||||
import editable from "../../Common/EditableUtility";
|
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
|
||||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
|
||||||
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||||
import { readDocument } from "../../Common/dataAccess/readDocument";
|
import { readDocument } from "../../Common/dataAccess/readDocument";
|
||||||
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||||
|
import editable from "../../Common/EditableUtility";
|
||||||
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||||
|
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
@@ -33,7 +30,6 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
import * as QueryUtils from "../../Utils/QueryUtils";
|
import * as QueryUtils from "../../Utils/QueryUtils";
|
||||||
import { extractPartitionKeyValues } from "../../Utils/QueryUtils";
|
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { useDialog } from "../Controls/Dialog";
|
import { useDialog } from "../Controls/Dialog";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
@@ -83,7 +79,6 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
private _resourceTokenPartitionKey: string;
|
private _resourceTokenPartitionKey: string;
|
||||||
private _isQueryCopilotSampleContainer: boolean;
|
private _isQueryCopilotSampleContainer: boolean;
|
||||||
private queryAbortController: AbortController;
|
private queryAbortController: AbortController;
|
||||||
private cancelQueryTimeoutID: NodeJS.Timeout;
|
|
||||||
|
|
||||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -351,22 +346,6 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Query first page of documents
|
|
||||||
* Select and query first document and display content
|
|
||||||
*/
|
|
||||||
private async autoPopulateContent(applyFilterButtonPressed?: boolean) {
|
|
||||||
// reset iterator
|
|
||||||
this._documentsIterator = this.createIterator();
|
|
||||||
// load documents
|
|
||||||
await this.loadNextPage(applyFilterButtonPressed);
|
|
||||||
|
|
||||||
// Select first document and load content
|
|
||||||
if (this.documentIds().length > 0) {
|
|
||||||
this.documentIds()[0].click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public onShowFilterClick(): Q.Promise<any> {
|
public onShowFilterClick(): Q.Promise<any> {
|
||||||
this.isFilterCreated(true);
|
this.isFilterCreated(true);
|
||||||
this.isFilterExpanded(true);
|
this.isFilterExpanded(true);
|
||||||
@@ -396,14 +375,15 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
public async refreshDocumentsGrid(applyFilterButtonPressed?: boolean): Promise<void> {
|
public async refreshDocumentsGrid(): Promise<void> {
|
||||||
// clear documents grid
|
// clear documents grid
|
||||||
this.documentIds([]);
|
this.documentIds([]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// reset iterator
|
// reset iterator
|
||||||
this._documentsIterator = this.createIterator();
|
this._documentsIterator = this.createIterator();
|
||||||
// load documents
|
// load documents
|
||||||
await this.autoPopulateContent(applyFilterButtonPressed);
|
await this.loadNextPage();
|
||||||
// collapse filter
|
// collapse filter
|
||||||
this.appliedFilter(this.filterContent());
|
this.appliedFilter(this.filterContent());
|
||||||
this.isFilterExpanded(false);
|
this.isFilterExpanded(false);
|
||||||
@@ -426,11 +406,6 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
this.queryAbortController.abort();
|
this.queryAbortController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO Doesn't seem to be used: remove?
|
|
||||||
* @param clickedDocumentId
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> {
|
public onDocumentIdClick(clickedDocumentId: DocumentId): Q.Promise<any> {
|
||||||
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
|
if (this.editorState() !== ViewModels.DocumentExplorerState.noDocumentSelected) {
|
||||||
return Q();
|
return Q();
|
||||||
@@ -480,7 +455,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
|
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
|
||||||
this.selectedDocumentContent.setBaseline(value);
|
this.selectedDocumentContent.setBaseline(value);
|
||||||
this.initialDocumentContent(value);
|
this.initialDocumentContent(value);
|
||||||
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues(
|
const partitionKeyValueArray = extractPartitionKey(
|
||||||
savedDocument,
|
savedDocument,
|
||||||
this.partitionKey as PartitionKeyDefinition,
|
this.partitionKey as PartitionKeyDefinition,
|
||||||
);
|
);
|
||||||
@@ -531,10 +506,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
const selectedDocumentId = this.selectedDocumentId();
|
const selectedDocumentId = this.selectedDocumentId();
|
||||||
const documentContent = JSON.parse(this.selectedDocumentContent());
|
const documentContent = JSON.parse(this.selectedDocumentContent());
|
||||||
|
|
||||||
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues(
|
const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition);
|
||||||
documentContent,
|
|
||||||
this.partitionKey as PartitionKeyDefinition,
|
|
||||||
);
|
|
||||||
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
|
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
|
||||||
|
|
||||||
this.isExecutionError(false);
|
this.isExecutionError(false);
|
||||||
@@ -652,7 +624,8 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
|
|
||||||
if (!this._documentsIterator) {
|
if (!this._documentsIterator) {
|
||||||
try {
|
try {
|
||||||
await this.autoPopulateContent();
|
this._documentsIterator = this.createIterator();
|
||||||
|
await this.loadNextPage();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
@@ -743,35 +716,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
this.initDocumentEditor(documentId, content);
|
this.initDocumentEditor(documentId, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadNextPage(applyFilterButtonClicked?: boolean): Q.Promise<any> {
|
public loadNextPage(): Q.Promise<any> {
|
||||||
this.isExecuting(true);
|
this.isExecuting(true);
|
||||||
this.isExecutionError(false);
|
this.isExecutionError(false);
|
||||||
let automaticallyCancelQueryAfterTimeout: boolean;
|
|
||||||
if (applyFilterButtonClicked && this.queryTimeoutEnabled()) {
|
|
||||||
const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout);
|
|
||||||
automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean(
|
|
||||||
StorageKey.AutomaticallyCancelQueryAfterTimeout,
|
|
||||||
);
|
|
||||||
const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => {
|
|
||||||
if (this.isExecuting()) {
|
|
||||||
if (automaticallyCancelQueryAfterTimeout) {
|
|
||||||
this.queryAbortController.abort();
|
|
||||||
} else {
|
|
||||||
useDialog
|
|
||||||
.getState()
|
|
||||||
.showOkCancelModalDialog(
|
|
||||||
QueryConstants.CancelQueryTitle,
|
|
||||||
format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached),
|
|
||||||
"Yes",
|
|
||||||
() => this.queryAbortController.abort(),
|
|
||||||
"No",
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, queryTimeout);
|
|
||||||
this.cancelQueryTimeoutID = cancelQueryTimeoutID;
|
|
||||||
}
|
|
||||||
return this._loadNextPageInternal()
|
return this._loadNextPageInternal()
|
||||||
.then(
|
.then(
|
||||||
(documentsIdsResponse = []) => {
|
(documentsIdsResponse = []) => {
|
||||||
@@ -827,15 +774,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.finally(() => {
|
.finally(() => this.isExecuting(false));
|
||||||
this.isExecuting(false);
|
|
||||||
if (applyFilterButtonClicked && this.queryTimeoutEnabled()) {
|
|
||||||
clearTimeout(this.cancelQueryTimeoutID);
|
|
||||||
if (!automaticallyCancelQueryAfterTimeout) {
|
|
||||||
useDialog.getState().closeDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
|
public onLoadMoreKeyInput = (source: any, event: KeyboardEvent): void => {
|
||||||
@@ -1013,8 +952,4 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private queryTimeoutEnabled(): boolean {
|
|
||||||
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
||||||
import { extractPartitionKeyValues } from "Utils/QueryUtils";
|
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -89,7 +88,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
|||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
(savedDocument: any) => {
|
(savedDocument: any) => {
|
||||||
const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues(
|
let partitionKeyArray = extractPartitionKey(
|
||||||
savedDocument,
|
savedDocument,
|
||||||
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
||||||
);
|
);
|
||||||
@@ -151,7 +150,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
|||||||
|
|
||||||
this.documentIds().forEach((documentId: DocumentId) => {
|
this.documentIds().forEach((documentId: DocumentId) => {
|
||||||
if (documentId.rid === updatedDocument._rid) {
|
if (documentId.rid === updatedDocument._rid) {
|
||||||
const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues(
|
const partitionKeyArray = extractPartitionKey(
|
||||||
updatedDocument,
|
updatedDocument,
|
||||||
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
this._getPartitionKeyDefinition() as PartitionKeyDefinition,
|
||||||
);
|
);
|
||||||
@@ -290,7 +289,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _hasShardKeySpecified(document: any): boolean {
|
private _hasShardKeySpecified(document: any): boolean {
|
||||||
return Boolean(extractPartitionKeyValues(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition));
|
return Boolean(extractPartitionKey(document, this._getPartitionKeyDefinition() as PartitionKeyDefinition));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getPartitionKeyDefinition(): DataModels.PartitionKey {
|
private _getPartitionKeyDefinition(): DataModels.PartitionKey {
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
|
||||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
|
||||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import SplitterLayout from "react-splitter-layout";
|
import SplitterLayout from "react-splitter-layout";
|
||||||
import "react-splitter-layout/lib/index.css";
|
import "react-splitter-layout/lib/index.css";
|
||||||
import { format } from "react-string-format";
|
|
||||||
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||||
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
@@ -84,7 +80,6 @@ interface IQueryTabStates {
|
|||||||
isExecuting: boolean;
|
isExecuting: boolean;
|
||||||
showCopilotSidebar: boolean;
|
showCopilotSidebar: boolean;
|
||||||
queryCopilotGeneratedQuery: string;
|
queryCopilotGeneratedQuery: string;
|
||||||
cancelQueryTimeoutID: NodeJS.Timeout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
||||||
@@ -112,13 +107,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
|
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
|
||||||
queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
|
queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
|
||||||
cancelQueryTimeoutID: undefined,
|
|
||||||
};
|
};
|
||||||
this.isCloseClicked = false;
|
this.isCloseClicked = false;
|
||||||
this.splitterId = this.props.tabId + "_splitter";
|
this.splitterId = this.props.tabId + "_splitter";
|
||||||
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
||||||
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
||||||
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
|
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
|
||||||
|
|
||||||
this.executeQueryButton = {
|
this.executeQueryButton = {
|
||||||
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -255,34 +250,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
this.setState({
|
this.setState({
|
||||||
isExecuting: true,
|
isExecuting: true,
|
||||||
});
|
});
|
||||||
let automaticallyCancelQueryAfterTimeout: boolean;
|
|
||||||
if (this.queryTimeoutEnabled()) {
|
|
||||||
const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout);
|
|
||||||
automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean(
|
|
||||||
StorageKey.AutomaticallyCancelQueryAfterTimeout,
|
|
||||||
);
|
|
||||||
const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => {
|
|
||||||
if (this.state.isExecuting) {
|
|
||||||
if (automaticallyCancelQueryAfterTimeout) {
|
|
||||||
this.queryAbortController.abort();
|
|
||||||
} else {
|
|
||||||
useDialog
|
|
||||||
.getState()
|
|
||||||
.showOkCancelModalDialog(
|
|
||||||
QueryConstants.CancelQueryTitle,
|
|
||||||
format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached),
|
|
||||||
"Yes",
|
|
||||||
() => this.queryAbortController.abort(),
|
|
||||||
"No",
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, queryTimeout);
|
|
||||||
this.setState({
|
|
||||||
cancelQueryTimeoutID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -306,14 +273,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
this.props.tabsBaseInstance.isExecuting(false);
|
this.props.tabsBaseInstance.isExecuting(false);
|
||||||
this.setState({
|
this.setState({
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
cancelQueryTimeoutID: undefined,
|
|
||||||
});
|
});
|
||||||
if (this.queryTimeoutEnabled()) {
|
|
||||||
clearTimeout(this.state.cancelQueryTimeoutID);
|
|
||||||
if (!automaticallyCancelQueryAfterTimeout) {
|
|
||||||
useDialog.getState().closeDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.togglesOnFocus();
|
this.togglesOnFocus();
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
@@ -445,10 +405,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
return this.state.sqlQueryEditorContent;
|
return this.state.sqlQueryEditorContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private queryTimeoutEnabled(): boolean {
|
|
||||||
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsubscribeCopilotSidebar: () => void;
|
private unsubscribeCopilotSidebar: () => void;
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { extractPartitionKeyValues } from "Utils/QueryUtils";
|
import { extractPartitionKey } from "@azure/cosmos";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { readDocument } from "../../Common/dataAccess/readDocument";
|
import { readDocument } from "../../Common/dataAccess/readDocument";
|
||||||
@@ -42,7 +42,7 @@ export default class ConflictId {
|
|||||||
}
|
}
|
||||||
this.partitionKeyProperty = container && container.partitionKeyProperty;
|
this.partitionKeyProperty = container && container.partitionKeyProperty;
|
||||||
this.partitionKey = container && container.partitionKey;
|
this.partitionKey = container && container.partitionKey;
|
||||||
this.partitionKeyValue = extractPartitionKeyValues(this.parsedContent, this.partitionKey as any);
|
this.partitionKeyValue = extractPartitionKey(this.parsedContent, this.partitionKey as any);
|
||||||
this.stringPartitionKeyValue = this.getPartitionKeyValueAsString();
|
this.stringPartitionKeyValue = this.getPartitionKeyValueAsString();
|
||||||
this.id = ko.observable(data.id);
|
this.id = ko.observable(data.id);
|
||||||
this.isDirty = ko.observable(false);
|
this.isDirty = ko.observable(false);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useDatabases } from "Explorer/useDatabases";
|
|||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
@@ -21,7 +22,7 @@ export const useDatabaseTreeNodes = (container: Explorer, isNotebookEnabled: boo
|
|||||||
className: "databaseHeader",
|
className: "databaseHeader",
|
||||||
children: [],
|
children: [],
|
||||||
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
||||||
contextMenu: undefined, // TODO Disable this for now as the actions don't work. ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
||||||
onExpanded: async () => {
|
onExpanded: async () => {
|
||||||
useSelectedNode.getState().setSelectedNode(database);
|
useSelectedNode.getState().setSelectedNode(database);
|
||||||
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import "../less/hostedexplorer.less";
|
|||||||
import { AuthType } from "./AuthType";
|
import { AuthType } from "./AuthType";
|
||||||
import { DatabaseAccount } from "./Contracts/DataModels";
|
import { DatabaseAccount } from "./Contracts/DataModels";
|
||||||
import "./Explorer/Menus/NavBar/MeControlComponent.less";
|
import "./Explorer/Menus/NavBar/MeControlComponent.less";
|
||||||
|
import { useAADAuth } from "./hooks/useAADAuth";
|
||||||
|
import { useConfig } from "./hooks/useConfig";
|
||||||
|
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
|
||||||
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
|
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
|
||||||
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
|
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
|
||||||
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
|
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
|
||||||
@@ -17,9 +20,6 @@ import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
|
|||||||
import "./Platform/Hosted/ConnectScreen.less";
|
import "./Platform/Hosted/ConnectScreen.less";
|
||||||
import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils";
|
import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils";
|
||||||
import "./Shared/appInsights";
|
import "./Shared/appInsights";
|
||||||
import { useAADAuth } from "./hooks/useAADAuth";
|
|
||||||
import { useConfig } from "./hooks/useConfig";
|
|
||||||
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
|
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
@@ -51,7 +51,6 @@ const App: React.FunctionComponent = () => {
|
|||||||
authType: AuthType.AAD,
|
authType: AuthType.AAD,
|
||||||
databaseAccount,
|
databaseAccount,
|
||||||
authorizationToken: armToken,
|
authorizationToken: armToken,
|
||||||
graphAuthorizationToken: graphToken
|
|
||||||
};
|
};
|
||||||
} else if (authType === AuthType.EncryptedToken) {
|
} else if (authType === AuthType.EncryptedToken) {
|
||||||
frameWindow.hostedConfig = {
|
frameWindow.hostedConfig = {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export interface AAD {
|
|||||||
authType: AuthType.AAD;
|
authType: AuthType.AAD;
|
||||||
databaseAccount: DatabaseAccount;
|
databaseAccount: DatabaseAccount;
|
||||||
authorizationToken: string;
|
authorizationToken: string;
|
||||||
graphAuthorizationToken: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectionString {
|
export interface ConnectionString {
|
||||||
|
|||||||
40
src/Main.tsx
40
src/Main.tsx
@@ -1,13 +1,13 @@
|
|||||||
// CSS Dependencies
|
// CSS Dependencies
|
||||||
import { initializeIcons, loadTheme } from "@fluentui/react";
|
import { initializeIcons, loadTheme } from "@fluentui/react";
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
|
||||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||||
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||||
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import { useCarousel } from "hooks/useCarousel";
|
import { useCarousel } from "hooks/useCarousel";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import "../externals/jquery-ui.min.css";
|
import "../externals/jquery-ui.min.css";
|
||||||
import "../externals/jquery-ui.min.js";
|
import "../externals/jquery-ui.min.js";
|
||||||
import "../externals/jquery-ui.structure.min.css";
|
import "../externals/jquery-ui.structure.min.css";
|
||||||
@@ -16,27 +16,27 @@ import "../externals/jquery.dataTables.min.css";
|
|||||||
import "../externals/jquery.typeahead.min.css";
|
import "../externals/jquery.typeahead.min.css";
|
||||||
import "../externals/jquery.typeahead.min.js";
|
import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
import { Platform } from "ConfigContext";
|
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import "../images/favicon.ico";
|
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
|
import "../images/favicon.ico";
|
||||||
|
import "../less/TableStyles/CustomizeColumns.less";
|
||||||
|
import "../less/TableStyles/EntityEditor.less";
|
||||||
|
import "../less/TableStyles/fulldatatables.less";
|
||||||
|
import "../less/TableStyles/queryBuilder.less";
|
||||||
|
import * as StyleConstants from "./Common/StyleConstants";
|
||||||
|
import { configContext, Platform } from "ConfigContext";
|
||||||
import "../less/documentDB.less";
|
import "../less/documentDB.less";
|
||||||
import "../less/forms.less";
|
import "../less/forms.less";
|
||||||
import "../less/infobox.less";
|
import "../less/infobox.less";
|
||||||
import "../less/menus.less";
|
import "../less/menus.less";
|
||||||
import "../less/messagebox.less";
|
import "../less/messagebox.less";
|
||||||
import "../less/resourceTree.less";
|
import "../less/resourceTree.less";
|
||||||
import "../less/TableStyles/CustomizeColumns.less";
|
|
||||||
import "../less/TableStyles/EntityEditor.less";
|
|
||||||
import "../less/TableStyles/fulldatatables.less";
|
|
||||||
import "../less/TableStyles/queryBuilder.less";
|
|
||||||
import "../less/tree.less";
|
import "../less/tree.less";
|
||||||
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
||||||
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
||||||
import * as StyleConstants from "./Common/StyleConstants";
|
|
||||||
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
import "./Explorer/Controls/Accordion/AccordionComponent.less";
|
||||||
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
||||||
import { Dialog } from "./Explorer/Controls/Dialog";
|
import { Dialog } from "./Explorer/Controls/Dialog";
|
||||||
@@ -55,11 +55,11 @@ import "./Explorer/Panes/PanelComponent.less";
|
|||||||
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
||||||
import "./Explorer/SplashScreen/SplashScreen.less";
|
import "./Explorer/SplashScreen/SplashScreen.less";
|
||||||
import { Tabs } from "./Explorer/Tabs/Tabs";
|
import { Tabs } from "./Explorer/Tabs/Tabs";
|
||||||
|
import "./Libs/jquery";
|
||||||
|
import "./Shared/appInsights";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
import "./Libs/jquery";
|
|
||||||
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
|
||||||
import "./Shared/appInsights";
|
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
@@ -72,7 +72,6 @@ const App: React.FunctionComponent = () => {
|
|||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
if (config?.platform === Platform.Fabric) {
|
if (config?.platform === Platform.Fabric) {
|
||||||
loadTheme(appThemeFabric);
|
loadTheme(appThemeFabric);
|
||||||
import("../less/documentDBFabric.less");
|
|
||||||
}
|
}
|
||||||
StyleConstants.updateStyles();
|
StyleConstants.updateStyles();
|
||||||
const explorer = useKnockoutExplorer(config?.platform);
|
const explorer = useKnockoutExplorer(config?.platform);
|
||||||
@@ -92,6 +91,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flexContainer" aria-hidden="false">
|
<div className="flexContainer" aria-hidden="false">
|
||||||
|
<LoadFabricOverrides />
|
||||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||||
<div id="freeTierTeachingBubble"> </div>
|
<div id="freeTierTeachingBubble"> </div>
|
||||||
{/* Main Command Bar - Start */}
|
{/* Main Command Bar - Start */}
|
||||||
@@ -141,8 +141,20 @@ const App: React.FunctionComponent = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mainElement = document.getElementById("Main");
|
ReactDOM.render(<App />, document.body);
|
||||||
ReactDOM.render(<App />, mainElement);
|
|
||||||
|
function LoadFabricOverrides(): JSX.Element {
|
||||||
|
if (configContext.platform === Platform.Fabric) {
|
||||||
|
const FabricStyle = React.lazy(() => import("./Platform/Fabric/FabricPlatform"));
|
||||||
|
return (
|
||||||
|
<React.Suspense fallback={<div></div>}>
|
||||||
|
<FabricStyle />
|
||||||
|
</React.Suspense>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function LoadingExplorer(): JSX.Element {
|
function LoadingExplorer(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
|||||||
7
src/Platform/Fabric/FabricPlatform.tsx
Normal file
7
src/Platform/Fabric/FabricPlatform.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
import "../../../less/documentDBFabric.less";
|
||||||
|
// This is a dummy export, allowing us to conditionally import documentDBFabric.less
|
||||||
|
// by lazy-importing this in Main.tsx (see LoadFabricOverrides() there)
|
||||||
|
export default function InitFabric() {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
|
||||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
|
|
||||||
import { MessageTypes } from "Contracts/MessageTypes";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { updateUserContext } from "UserContext";
|
|
||||||
|
|
||||||
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
|
|
||||||
let timeoutId: NodeJS.Timeout;
|
|
||||||
|
|
||||||
// Prevents multiple parallel requests
|
|
||||||
let isRequestPending = false;
|
|
||||||
|
|
||||||
export const requestDatabaseResourceTokens = (): void => {
|
|
||||||
if (isRequestPending) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Make Fabric return the message id so we can handle this promise
|
|
||||||
isRequestPending = true;
|
|
||||||
sendCachedDataMessage<FabricDatabaseConnectionInfo>(MessageTypes.GetAllResourceTokens, []);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleRequestDatabaseResourceTokensResponse = (
|
|
||||||
explorer: Explorer,
|
|
||||||
fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo,
|
|
||||||
): void => {
|
|
||||||
isRequestPending = false;
|
|
||||||
updateUserContext({ fabricDatabaseConnectionInfo });
|
|
||||||
scheduleRefreshDatabaseResourceToken();
|
|
||||||
explorer.refreshAllDatabases();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check token validity and schedule a refresh if necessary
|
|
||||||
* @param tokenTimestamp
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const scheduleRefreshDatabaseResourceToken = (): void => {
|
|
||||||
if (timeoutId !== undefined) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
timeoutId = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
requestDatabaseResourceTokens();
|
|
||||||
}, TOKEN_VALIDITY_MS);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
|
|
||||||
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
|
|
||||||
requestDatabaseResourceTokens();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
jest.mock("../../../hooks/useDirectories");
|
jest.mock("../../../hooks/useDirectories");
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import { extractFeatures } from "Platform/Hosted/extractFeatures";
|
|
||||||
import { updateUserContext, userContext } from "UserContext";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ConnectExplorer } from "./ConnectExplorer";
|
import { ConnectExplorer } from "./ConnectExplorer";
|
||||||
|
|
||||||
@@ -18,24 +16,3 @@ it("shows the connect form", () => {
|
|||||||
fireEvent.click(screen.getByText("Connect to your account with connection string"));
|
fireEvent.click(screen.getByText("Connect to your account with connection string"));
|
||||||
expect(screen.queryByPlaceholderText("Please enter a connection string")).toBeDefined();
|
expect(screen.queryByPlaceholderText("Please enter a connection string")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides the connection string link when feature.disableConnectionStringLogin is true", () => {
|
|
||||||
const connectionString = "fakeConnectionString";
|
|
||||||
const login = jest.fn();
|
|
||||||
const setConnectionString = jest.fn();
|
|
||||||
const setEncryptedToken = jest.fn();
|
|
||||||
const setAuthType = jest.fn();
|
|
||||||
const oldFeatures = userContext.features;
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
"feature.disableConnectionStringLogin": "true",
|
|
||||||
});
|
|
||||||
|
|
||||||
const testFeatures = extractFeatures(params);
|
|
||||||
updateUserContext({ features: testFeatures });
|
|
||||||
|
|
||||||
render(<ConnectExplorer {...{ login, setEncryptedToken, setAuthType, connectionString, setConnectionString }} />);
|
|
||||||
expect(screen.queryByPlaceholderText("Connect to your account with connection string")).toBeNull();
|
|
||||||
|
|
||||||
updateUserContext({ features: oldFeatures });
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||||
import ErrorImage from "../../../../images/error.svg";
|
import ErrorImage from "../../../../images/error.svg";
|
||||||
@@ -30,18 +29,6 @@ export const fetchEncryptedToken = async (connectionString: string): Promise<str
|
|||||||
return decodeURIComponent(result.readWrite || result.read);
|
return decodeURIComponent(result.readWrite || result.read);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise<boolean> => {
|
|
||||||
const headers = new Headers();
|
|
||||||
headers.append(HttpHeaders.connectionString, connectionString);
|
|
||||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
|
|
||||||
const response = await fetch(url, { headers, method: "POST" });
|
|
||||||
if (!response.ok) {
|
|
||||||
throw response;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await response.text()) === "True";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
||||||
setEncryptedToken,
|
setEncryptedToken,
|
||||||
login,
|
login,
|
||||||
@@ -50,8 +37,6 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
setConnectionString,
|
setConnectionString,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
|
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
|
||||||
const [errorMessage, setErrorMessage] = React.useState("");
|
|
||||||
const enableConnectionStringLogin = !userContext.features.disableConnectionStringLogin;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
|
<div id="connectExplorer" className="connectExplorerContainer" style={{ display: "flex" }}>
|
||||||
@@ -61,17 +46,11 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
<img src={ConnectImage} alt="Azure Cosmos DB" />
|
<img src={ConnectImage} alt="Azure Cosmos DB" />
|
||||||
</p>
|
</p>
|
||||||
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
|
<p className="welcomeText">Welcome to Azure Cosmos DB</p>
|
||||||
{isFormVisible && enableConnectionStringLogin ? (
|
{isFormVisible ? (
|
||||||
<form
|
<form
|
||||||
id="connectWithConnectionString"
|
id="connectWithConnectionString"
|
||||||
onSubmit={async (event) => {
|
onSubmit={async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setErrorMessage("");
|
|
||||||
|
|
||||||
if (await isAccountRestrictedForConnectionStringLogin(connectionString)) {
|
|
||||||
setErrorMessage("This account has been blocked from connection-string login.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isResourceTokenConnectionString(connectionString)) {
|
if (isResourceTokenConnectionString(connectionString)) {
|
||||||
setAuthType(AuthType.ResourceToken);
|
setAuthType(AuthType.ResourceToken);
|
||||||
@@ -95,12 +74,10 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
setConnectionString(event.target.value);
|
setConnectionString(event.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{errorMessage.length > 0 && (
|
<span className="errorDetailsInfoTooltip" style={{ display: "none" }}>
|
||||||
<span className="errorDetailsInfoTooltip">
|
<img className="errorImg" src={ErrorImage} alt="Error notification" />
|
||||||
<img className="errorImg" src={ErrorImage} alt="Error notification" />
|
<span className="errorDetails"></span>
|
||||||
<span className="errorDetails">{errorMessage}</span>
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="connectExplorerContent">
|
<p className="connectExplorerContent">
|
||||||
<input className="filterbtnstyle" type="submit" value="Connect" />
|
<input className="filterbtnstyle" type="submit" value="Connect" />
|
||||||
@@ -112,11 +89,9 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div id="connectWithAad">
|
<div id="connectWithAad">
|
||||||
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
|
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
|
||||||
{enableConnectionStringLogin && (
|
<p className="switchConnectTypeText" onClick={showForm}>
|
||||||
<p className="switchConnectTypeText" onClick={showForm}>
|
Connect to your account with connection string
|
||||||
Connect to your account with connection string
|
</p>
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export type Features = {
|
|||||||
readonly enableCopilotFullSchema: boolean;
|
readonly enableCopilotFullSchema: boolean;
|
||||||
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
readonly copilotChatFixedMonacoEditorHeight: boolean;
|
||||||
readonly enablePriorityBasedExecution: boolean;
|
readonly enablePriorityBasedExecution: boolean;
|
||||||
readonly disableConnectionStringLogin: boolean;
|
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -115,7 +114,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
|
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
|
||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,9 +208,3 @@ export class FreeTierLimits {
|
|||||||
public static RU: number = 1000;
|
public static RU: number = 1000;
|
||||||
public static Storage: number = 25;
|
public static Storage: number = 25;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryConstants {
|
|
||||||
public static readonly CancelQueryTitle: string = "Cancel query";
|
|
||||||
public static readonly CancelQuerySubTextTemplate: string = "{0} Do you want to cancel this query?";
|
|
||||||
public static readonly CancelQueryTimeoutThresholdReached: string = "The query timeout threshold has been reached.";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import * as SessionStorageUtility from "./SessionStorageUtility";
|
|||||||
export { LocalStorageUtility, SessionStorageUtility };
|
export { LocalStorageUtility, SessionStorageUtility };
|
||||||
export enum StorageKey {
|
export enum StorageKey {
|
||||||
ActualItemPerPage,
|
ActualItemPerPage,
|
||||||
QueryTimeoutEnabled,
|
|
||||||
QueryTimeout,
|
|
||||||
AutomaticallyCancelQueryAfterTimeout,
|
|
||||||
ContainerPaginationEnabled,
|
ContainerPaginationEnabled,
|
||||||
CustomItemPerPage,
|
CustomItemPerPage,
|
||||||
DatabaseAccountId,
|
DatabaseAccountId,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
|
|
||||||
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -48,7 +47,6 @@ export interface VCoreMongoConnectionParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UserContext {
|
interface UserContext {
|
||||||
readonly fabricDatabaseConnectionInfo?: FabricDatabaseConnectionInfo;
|
|
||||||
readonly authType?: AuthType;
|
readonly authType?: AuthType;
|
||||||
readonly masterKey?: string;
|
readonly masterKey?: string;
|
||||||
readonly subscriptionId?: string;
|
readonly subscriptionId?: string;
|
||||||
@@ -79,7 +77,6 @@ interface UserContext {
|
|||||||
collectionCreationDefaults: CollectionCreationDefaults;
|
collectionCreationDefaults: CollectionCreationDefaults;
|
||||||
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
||||||
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
|
||||||
readonly accountRestrictedFromUser?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||||
@@ -172,4 +169,3 @@ function apiType(account: DatabaseAccount | undefined): ApiType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { updateUserContext, userContext };
|
export { updateUserContext, userContext };
|
||||||
|
|
||||||
|
|||||||
@@ -60,27 +60,3 @@ export function getMsalInstance() {
|
|||||||
const msalInstance = new msal.PublicClientApplication(config);
|
const msalInstance = new msal.PublicClientApplication(config);
|
||||||
return msalInstance;
|
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, undefined, next) => {
|
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) => {
|
||||||
if (isRelevantRequest(requestContext)) {
|
if (isRelevantRequest(requestContext)) {
|
||||||
const priorityLevel: PriorityLevel = getPriorityLevel();
|
const priorityLevel: PriorityLevel = getPriorityLevel();
|
||||||
requestContext.headers["x-ms-cosmos-priority-level"] = priorityLevel as string;
|
requestContext.headers["x-ms-cosmos-priority-level"] = priorityLevel as string;
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { PartitionKey, PartitionKeyDefinition, PartitionKeyKind } from "@azure/cosmos";
|
|
||||||
import * as Q from "q";
|
import * as Q from "q";
|
||||||
import * as sinon from "sinon";
|
import * as sinon from "sinon";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import * as QueryUtils from "./QueryUtils";
|
import * as QueryUtils from "./QueryUtils";
|
||||||
import { extractPartitionKeyValues } from "./QueryUtils";
|
|
||||||
|
|
||||||
describe("Query Utils", () => {
|
describe("Query Utils", () => {
|
||||||
const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => {
|
const generatePartitionKeyForPath = (path: string): DataModels.PartitionKey => {
|
||||||
@@ -96,69 +94,4 @@ describe("Query Utils", () => {
|
|||||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("extractPartitionKey", () => {
|
|
||||||
const documentContent = {
|
|
||||||
"Volcano Name": "Adams",
|
|
||||||
Country: "United States",
|
|
||||||
Region: "US-Washington",
|
|
||||||
Location: {
|
|
||||||
type: "Point",
|
|
||||||
coordinates: [-121.49, 46.206],
|
|
||||||
},
|
|
||||||
Elevation: 3742,
|
|
||||||
Type: "Stratovolcano",
|
|
||||||
Status: "Tephrochronology",
|
|
||||||
"Last Known Eruption": "Last known eruption from A.D. 1-1499, inclusive",
|
|
||||||
id: "9e3c494e-8367-3f50-1f56-8c6fcb961363",
|
|
||||||
_rid: "xzo0AJRYUxUFAAAAAAAAAA==",
|
|
||||||
_self: "dbs/xzo0AA==/colls/xzo0AJRYUxU=/docs/xzo0AJRYUxUFAAAAAAAAAA==/",
|
|
||||||
_etag: '"ce00fa43-0000-0100-0000-652840440000"',
|
|
||||||
_attachments: "attachments/",
|
|
||||||
_ts: 1697136708,
|
|
||||||
};
|
|
||||||
|
|
||||||
it("should extract single partition key value", () => {
|
|
||||||
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
|
|
||||||
kind: PartitionKeyKind.Hash,
|
|
||||||
paths: ["/Elevation"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
|
||||||
documentContent,
|
|
||||||
singlePartitionKeyDefinition,
|
|
||||||
);
|
|
||||||
expect(partitionKeyValues.length).toBe(1);
|
|
||||||
expect(partitionKeyValues[0]).toEqual(3742);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract two partition key values", () => {
|
|
||||||
const multiPartitionKeyDefinition: PartitionKeyDefinition = {
|
|
||||||
kind: PartitionKeyKind.MultiHash,
|
|
||||||
paths: ["/Type", "/Status"],
|
|
||||||
};
|
|
||||||
const expectedPartitionKeyValues: string[] = ["Stratovolcano", "Tephrochronology"];
|
|
||||||
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
|
||||||
documentContent,
|
|
||||||
multiPartitionKeyDefinition,
|
|
||||||
);
|
|
||||||
expect(partitionKeyValues.length).toBe(2);
|
|
||||||
expect(expectedPartitionKeyValues).toContain(documentContent["Type"]);
|
|
||||||
expect(expectedPartitionKeyValues).toContain(documentContent["Status"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should extract no partition key values", () => {
|
|
||||||
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
|
|
||||||
kind: PartitionKeyKind.Hash,
|
|
||||||
paths: ["/InvalidPartitionKeyPath"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
|
|
||||||
documentContent,
|
|
||||||
singlePartitionKeyDefinition,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(partitionKeyValues.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
|
||||||
@@ -83,22 +82,3 @@ export const queryPagesUntilContentPresent = async (
|
|||||||
|
|
||||||
return await doRequest(firstItemIndex);
|
return await doRequest(firstItemIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
export const extractPartitionKeyValues = (
|
|
||||||
documentContent: any,
|
|
||||||
partitionKeyDefinition: PartitionKeyDefinition,
|
|
||||||
): PartitionKey[] => {
|
|
||||||
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const partitionKeyValues: PartitionKey[] = [];
|
|
||||||
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
|
|
||||||
const partitionKeyPathWithoutSlash: string = partitionKeyPath.substring(1);
|
|
||||||
if (documentContent[partitionKeyPathWithoutSlash]) {
|
|
||||||
partitionKeyValues.push(documentContent[partitionKeyPathWithoutSlash]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return partitionKeyValues;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -8,7 +8,5 @@
|
|||||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body></body>
|
||||||
<div id="Main" style="height: 100%"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import { createUri } from "Common/UrlUtility";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import { FabricDatabaseConnectionInfo, FabricMessage } from "Contracts/FabricContract";
|
import { FabricMessage } from "Contracts/FabricContract";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import {
|
import { fetchEncryptedToken } from "Platform/Hosted/Components/ConnectExplorer";
|
||||||
handleRequestDatabaseResourceTokensResponse,
|
|
||||||
scheduleRefreshDatabaseResourceToken,
|
|
||||||
} from "Platform/Fabric/FabricUtil";
|
|
||||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||||
|
import { fetchAccessData } from "hooks/usePortalAccessToken";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { AccountKind, Flights } from "../Common/Constants";
|
import { AccountKind, Flights } from "../Common/Constants";
|
||||||
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
||||||
import { handleCachedDataMessage, sendMessage, sendReadyMessage } from "../Common/MessageHandler";
|
import { sendMessage, sendReadyMessage } from "../Common/MessageHandler";
|
||||||
import { Platform, configContext, updateConfigContext } from "../ConfigContext";
|
import { Platform, configContext, updateConfigContext } from "../ConfigContext";
|
||||||
import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
|
import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
@@ -36,7 +34,7 @@ import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
|||||||
import { CollectionCreation } from "../Shared/Constants";
|
import { CollectionCreation } from "../Shared/Constants";
|
||||||
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
||||||
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
|
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
|
||||||
import { getAuthorizationHeader, getMsalInstance, isAccountRestrictedFromUser } from "../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||||
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
|
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
|
||||||
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
|
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
|
||||||
@@ -102,37 +100,70 @@ async function configureFabric(): Promise<Explorer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data: FabricMessage = event.data?.data;
|
const data: FabricMessage = event.data?.data;
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case "initialize": {
|
case "initialize": {
|
||||||
const fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo = {
|
// TODO For now, retrieve info from session storage. Replace with info injected into Data Explorer
|
||||||
endpoint: data.message.endpoint,
|
const connectionString = data.connectionString ?? sessionStorage.getItem("connectionString");
|
||||||
databaseId: data.message.databaseId,
|
if (!connectionString) {
|
||||||
resourceTokens: data.message.resourceTokens as { [resourceId: string]: string },
|
console.error("No connection string found in session storage");
|
||||||
resourceTokensTimestamp: data.message.resourceTokensTimestamp,
|
return undefined;
|
||||||
};
|
}
|
||||||
explorer = await createExplorerFabric(fabricDatabaseConnectionInfo);
|
const encryptedToken = await fetchEncryptedToken(connectionString);
|
||||||
resolve(explorer);
|
// TODO Duplicated from useTokenMetadata
|
||||||
|
const encryptedTokenMetadata = await fetchAccessData(encryptedToken);
|
||||||
|
|
||||||
explorer.refreshAllDatabases().then(() => {
|
const hostedConfig: EncryptedToken = {
|
||||||
openFirstContainer(explorer, fabricDatabaseConnectionInfo.databaseId);
|
authType: AuthType.EncryptedToken,
|
||||||
});
|
encryptedToken,
|
||||||
scheduleRefreshDatabaseResourceToken();
|
encryptedTokenMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
explorer = await configureWithEncryptedToken(hostedConfig);
|
||||||
|
resolve(explorer);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "newContainer":
|
case "newContainer":
|
||||||
explorer.onNewCollectionClicked();
|
explorer.onNewCollectionClicked();
|
||||||
break;
|
break;
|
||||||
case "authorizationToken": {
|
case "openTab": {
|
||||||
handleCachedDataMessage(data);
|
// Expand database first
|
||||||
break;
|
const databaseName = sessionStorage.getItem("openDatabaseName") ?? data.databaseName;
|
||||||
}
|
const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
|
||||||
case "allResourceTokens": {
|
if (database) {
|
||||||
// TODO call handleCachedDataMessage when Fabric echoes message id back
|
await database.expandDatabase();
|
||||||
handleRequestDatabaseResourceTokensResponse(explorer, data.message as FabricDatabaseConnectionInfo);
|
useDatabases.getState().updateDatabase(database);
|
||||||
|
useSelectedNode.getState().setSelectedNode(database);
|
||||||
|
|
||||||
|
let collectionResourceId = data.collectionName;
|
||||||
|
if (collectionResourceId === undefined) {
|
||||||
|
// Pick first collection if collectionName not specified in message
|
||||||
|
collectionResourceId = database.collections()[0]?.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectionResourceId !== undefined) {
|
||||||
|
// Expand collection
|
||||||
|
const collection = database.collections().find((coll) => coll.id() === collectionResourceId);
|
||||||
|
collection.expandCollection();
|
||||||
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
|
|
||||||
|
handleOpenAction(
|
||||||
|
{
|
||||||
|
actionType: ActionType.OpenCollectionTab,
|
||||||
|
databaseResourceId: databaseName,
|
||||||
|
collectionResourceId: data.collectionName,
|
||||||
|
tabKind: TabKind.SQLDocuments,
|
||||||
|
} as DataExplorerAction,
|
||||||
|
useDatabases.getState().databases,
|
||||||
|
explorer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -147,41 +178,6 @@ async function configureFabric(): Promise<Explorer> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const openFirstContainer = async (explorer: Explorer, databaseName: string, collectionName?: string) => {
|
|
||||||
// Expand database first
|
|
||||||
databaseName = sessionStorage.getItem("openDatabaseName") ?? databaseName;
|
|
||||||
const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
|
|
||||||
if (database) {
|
|
||||||
await database.expandDatabase();
|
|
||||||
useDatabases.getState().updateDatabase(database);
|
|
||||||
useSelectedNode.getState().setSelectedNode(database);
|
|
||||||
|
|
||||||
let collectionResourceId = collectionName;
|
|
||||||
if (collectionResourceId === undefined) {
|
|
||||||
// Pick first collection if collectionName not specified in message
|
|
||||||
collectionResourceId = database.collections()[0]?.id();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectionResourceId !== undefined) {
|
|
||||||
// Expand collection
|
|
||||||
const collection = database.collections().find((coll) => coll.id() === collectionResourceId);
|
|
||||||
collection.expandCollection();
|
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
|
||||||
|
|
||||||
handleOpenAction(
|
|
||||||
{
|
|
||||||
actionType: ActionType.OpenCollectionTab,
|
|
||||||
databaseResourceId: databaseName,
|
|
||||||
collectionResourceId: collectionName,
|
|
||||||
tabKind: TabKind.SQLDocuments,
|
|
||||||
} as DataExplorerAction,
|
|
||||||
useDatabases.getState().databases,
|
|
||||||
explorer,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function configureHosted(): Promise<Explorer> {
|
async function configureHosted(): Promise<Explorer> {
|
||||||
const win = window as unknown as HostedExplorerChildFrame;
|
const win = window as unknown as HostedExplorerChildFrame;
|
||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
@@ -227,11 +223,9 @@ async function configureHosted(): Promise<Explorer> {
|
|||||||
|
|
||||||
async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
||||||
// TODO: Refactor. updateUserContext needs to be called twice because listKeys below depends on userContext.authorizationToken
|
// 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({
|
updateUserContext({
|
||||||
authType: AuthType.AAD,
|
authType: AuthType.AAD,
|
||||||
authorizationToken: `Bearer ${config.authorizationToken}`,
|
authorizationToken: `Bearer ${config.authorizationToken}`,
|
||||||
accountRestrictedFromUser
|
|
||||||
});
|
});
|
||||||
const account = config.databaseAccount;
|
const account = config.databaseAccount;
|
||||||
const accountResourceId = account.id;
|
const accountResourceId = account.id;
|
||||||
@@ -321,25 +315,6 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
|
|||||||
return explorer;
|
return explorer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createExplorerFabric(fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo): Explorer {
|
|
||||||
updateUserContext({
|
|
||||||
fabricDatabaseConnectionInfo,
|
|
||||||
authType: AuthType.ConnectionString,
|
|
||||||
databaseAccount: {
|
|
||||||
id: "",
|
|
||||||
location: "",
|
|
||||||
type: "",
|
|
||||||
name: "Mounted",
|
|
||||||
kind: AccountKind.Default,
|
|
||||||
properties: {
|
|
||||||
documentEndpoint: fabricDatabaseConnectionInfo.endpoint,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const explorer = new Explorer();
|
|
||||||
return explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function configureWithEncryptedToken(config: EncryptedToken): Explorer {
|
function configureWithEncryptedToken(config: EncryptedToken): Explorer {
|
||||||
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
|
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
|||||||
|
|
||||||
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
|
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
|
||||||
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
|
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
|
||||||
closeFeedbackModal: () => set({ showFeedbackModal: false }),
|
closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }),
|
||||||
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
|
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
|
||||||
set({ hideFeedbackModalForLikedQueries }),
|
set({ hideFeedbackModalForLikedQueries }),
|
||||||
refreshCorrelationId: () => set({ correlationId: guid() }),
|
refreshCorrelationId: () => set({ correlationId: guid() }),
|
||||||
|
|||||||
@@ -6,15 +6,6 @@
|
|||||||
<mimeMap fileExtension="woff" mimeType="application/font-woff" />
|
<mimeMap fileExtension="woff" mimeType="application/font-woff" />
|
||||||
</staticContent>
|
</staticContent>
|
||||||
<rewrite>
|
<rewrite>
|
||||||
<rules>
|
|
||||||
<rule name="AAD-Redirect" stopProcessing="true">
|
|
||||||
<match url="^aad" ignoreCase="true"/>
|
|
||||||
<conditions>
|
|
||||||
<add input="{HTTP_HOST}" pattern="^cosmos.azure.com" />
|
|
||||||
</conditions>
|
|
||||||
<action type="Redirect" url="/?feature.enableAadDataPlane=true&feature.disableConnectionStringLogin=true" redirectType="Permanent" />
|
|
||||||
</rule>
|
|
||||||
</rules>
|
|
||||||
<outboundRules>
|
<outboundRules>
|
||||||
<rule name="Strict-Transport-Security" enabled="true">
|
<rule name="Strict-Transport-Security" enabled="true">
|
||||||
<match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
|
<match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
|
||||||
|
|||||||
Reference in New Issue
Block a user