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