Compare commits

..

3 Commits

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

View File

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

13
package-lock.json generated
View File

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

View File

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

View File

@@ -177,7 +177,6 @@ export class HttpHeaders {
public static activityId: string = "x-ms-activity-id"; public static activityId: string = "x-ms-activity-id";
public static apiType: string = "x-ms-cosmos-apitype"; public static apiType: string = "x-ms-cosmos-apitype";
public static authorization: string = "authorization"; public static authorization: string = "authorization";
public static graphAuthorization: string = "graph-authorization";
public static collectionIndexTransformationProgress: string = public static collectionIndexTransformationProgress: string =
"x-ms-documentdb-collection-index-transformation-progress"; "x-ms-documentdb-collection-index-transformation-progress";
public static continuation: string = "x-ms-continuation"; public static continuation: string = "x-ms-continuation";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -64,7 +64,6 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`, `^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
`^https:\\/\\/.*\\.powerbi\\.com$`, `^https:\\/\\/.*\\.powerbi\\.com$`,
`^https:\\/\\/.*\\.analysis-df\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
], // Webpack injects this at build time ], // Webpack injects this at build time
gitSha: process.env.GIT_SHA, gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/", hostedExplorerURL: "https://cosmos.azure.com/",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
import { IGalleryItem } from "Juno/JunoClient"; import { IGalleryItem } from "Juno/JunoClient";
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout"; import * as ko from "knockout";
@@ -380,13 +379,6 @@ export default class Explorer {
}; };
public onRefreshResourcesClick = (): void => { public onRefreshResourcesClick = (): void => {
if (configContext.platform === Platform.Fabric) {
// Requesting the tokens will trigger a refresh of the databases
// TODO: Once the id is returned from Fabric, we can await this call and then refresh the databases here
requestDatabaseResourceTokens();
return;
}
userContext.authType === AuthType.ResourceToken userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases(); : this.refreshAllDatabases();

View File

@@ -50,36 +50,31 @@ export function createStaticCommandBarButtons(
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState); return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
} }
const newCollectionBtn = createNewCollectionGroup(container);
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
// Avoid starting with a divider buttons.push(newCollectionBtn);
const addDivider = () => { if (
if (buttons.length > 0) { configContext.platform !== Platform.Fabric &&
userContext.apiType !== "Tables" &&
userContext.apiType !== "Cassandra"
) {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
buttons.push(createDivider()); buttons.push(createDivider());
} buttons.push(addSynapseLink);
};
if (configContext.platform !== Platform.Fabric) {
const newCollectionBtn = createNewCollectionGroup(container);
buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
addDivider();
buttons.push(addSynapseLink);
}
}
if (userContext.apiType !== "Tables") {
newCollectionBtn.children = [createNewCollectionGroup(container)];
const newDatabaseBtn = createNewDatabase(container);
newCollectionBtn.children.push(newDatabaseBtn);
} }
} }
if (userContext.apiType !== "Tables") {
newCollectionBtn.children = [createNewCollectionGroup(container)];
const newDatabaseBtn = createNewDatabase(container);
newCollectionBtn.children.push(newDatabaseBtn);
}
if (useNotebook.getState().isNotebookEnabled) { if (useNotebook.getState().isNotebookEnabled) {
addDivider(); buttons.push(createDivider());
const notebookButtons: CommandButtonComponentProps[] = []; const notebookButtons: CommandButtonComponentProps[] = [];
const newNotebookButton = createNewNotebookButton(container); const newNotebookButton = createNewNotebookButton(container);
@@ -133,7 +128,7 @@ export function createStaticCommandBarButtons(
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isQuerySupported) { if (isQuerySupported) {
addDivider(); buttons.push(createDivider());
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState); const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
buttons.push(newSqlQueryBtn); buttons.push(newSqlQueryBtn);
} }

View File

@@ -114,8 +114,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
super(props); super(props);
this.state = { this.state = {
createNewDatabase: createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "", newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
isSharedThroughputChecked: this.getSharedThroughputDefault(), isSharedThroughputChecked: this.getSharedThroughputDefault(),
selectedDatabaseId: selectedDatabaseId:
@@ -275,38 +274,36 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</TooltipHost> </TooltipHost>
</Stack> </Stack>
{configContext.platform !== Platform.Fabric && ( <Stack horizontal verticalAlign="center">
<Stack horizontal verticalAlign="center"> <div role="radiogroup">
<div role="radiogroup"> <input
<input className="panelRadioBtn"
className="panelRadioBtn" checked={this.state.createNewDatabase}
checked={this.state.createNewDatabase} aria-label="Create new database"
aria-label="Create new database" aria-checked={this.state.createNewDatabase}
aria-checked={this.state.createNewDatabase} name="databaseType"
name="databaseType" type="radio"
type="radio" role="radio"
role="radio" id="databaseCreateNew"
id="databaseCreateNew" tabIndex={0}
tabIndex={0} onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)} />
/> <span className="panelRadioBtnLabel">Create new</span>
<span className="panelRadioBtnLabel">Create new</span>
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.createNewDatabase} checked={!this.state.createNewDatabase}
aria-label="Use existing database" aria-label="Use existing database"
aria-checked={!this.state.createNewDatabase} aria-checked={!this.state.createNewDatabase}
name="databaseType" name="databaseType"
type="radio" type="radio"
role="radio" role="radio"
tabIndex={0} tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)} onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/> />
<span className="panelRadioBtnLabel">Use existing</span> <span className="panelRadioBtnLabel">Use existing</span>
</div> </div>
</Stack> </Stack>
)}
{this.state.createNewDatabase && ( {this.state.createNewDatabase && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -102,7 +102,7 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("should not submit submission if required description field is null", () => { it("should submit submission", () => {
const explorer = new Explorer(); const explorer = new Explorer();
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />); const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
@@ -110,24 +110,12 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
submitButton.simulate("click"); submitButton.simulate("click");
wrapper.setProps({}); wrapper.setProps({});
expect(SubmitFeedback).toHaveBeenCalledTimes(0);
});
it("should submit submission", () => {
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
const explorer = new Explorer();
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
const submitButton = wrapper.find("form");
submitButton.simulate("submit");
wrapper.setProps({});
expect(SubmitFeedback).toHaveBeenCalledTimes(1); expect(SubmitFeedback).toHaveBeenCalledTimes(1);
expect(SubmitFeedback).toHaveBeenCalledWith({ expect(SubmitFeedback).toHaveBeenCalledWith({
params: { params: {
likeQuery: false, likeQuery: false,
generatedQuery: "test query", generatedQuery: "",
userPrompt: "test prompt", userPrompt: "",
description: "", description: "",
contact: getUserEmail(), contact: getUserEmail(),
}, },

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import { import {
Callout, Callout,
CommandBarButton, CommandBarButton,
@@ -140,7 +141,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
// Filter suggested prompts // Filter suggested prompts
const filteredSuggested = suggestedPrompts.filter((prompt) => const filteredSuggested = suggestedPrompts.filter((prompt) =>
prompt.text.toLowerCase().includes(value.toLowerCase()), prompt.text.toLowerCase().includes(value.toLowerCase())
); );
setFilteredSuggestedPrompts(filteredSuggested); setFilteredSuggestedPrompts(filteredSuggested);
}; };
@@ -150,7 +151,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim()); const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
const updatedHistories = existingHistories.filter( const updatedHistories = existingHistories.filter(
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase(), (history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase()
); );
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)]; const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
@@ -237,7 +238,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
const showTeachingBubble = (): void => { const showTeachingBubble = (): void => {
if (!inputEdited.current) { if (!inputEdited.current) {
setTimeout(() => { setTimeout(() => {
if (!inputEdited.current && !isWelcomModalVisible()) { if (!inputEdited.current) {
toggleCopilotTeachingBubbleVisible(); toggleCopilotTeachingBubbleVisible();
inputEdited.current = true; inputEdited.current = true;
} }
@@ -245,10 +246,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
} }
}; };
const isWelcomModalVisible = (): boolean => {
return localStorage.getItem("hideWelcomeModal") !== "true";
};
const clearFeedback = () => { const clearFeedback = () => {
resetButtonState(); resetButtonState();
resetQueryResults(); resetQueryResults();
@@ -301,7 +298,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setShowSamplePrompts(true); setShowSamplePrompts(true);
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && userPrompt) { if (e.key === "Enter") {
inputEdited.current = true; inputEdited.current = true;
startGenerateQueryProcess(); startGenerateQueryProcess();
} }
@@ -537,7 +534,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
iconProps={{ iconName: "Copy" }} iconProps={{ iconName: "Copy" }}
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }} style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
> >
Copy query Copy code
</CommandBarButton> </CommandBarButton>
<CommandBarButton <CommandBarButton
onClick={() => { onClick={() => {
@@ -546,11 +543,11 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
iconProps={{ iconName: "Delete" }} iconProps={{ iconName: "Delete" }}
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }} style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
> >
Delete query Delete code
</CommandBarButton> </CommandBarButton>
</Stack> </Stack>
)} )}
<WelcomeModal visible={isWelcomModalVisible()} /> <WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />} {isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
{query !== "" && query.trim().length !== 0 && ( {query !== "" && query.trim().length !== 0 && (
<DeletePopup <DeletePopup

View File

@@ -21,13 +21,8 @@ import * as StringUtility from "../../Shared/StringUtility";
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => { export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot(); const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot();
const cachedCopilotToggleStatus: string = localStorage.getItem( const cachedCopilotToggleStatus = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`);
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, const [copilotActive, setCopilotActive] = useState<boolean>(StringUtility.toBoolean(cachedCopilotToggleStatus));
);
const copilotInitialActive: boolean = cachedCopilotToggleStatus
? StringUtility.toBoolean(cachedCopilotToggleStatus)
: true;
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
const getCommandbarButtons = (): CommandButtonComponentProps[] => { const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query"; const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
@@ -92,7 +87,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
<QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar> <QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar>
)} )}
<Stack className="tabPaneContentContainer"> <Stack className="tabPaneContentContainer">
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}> <SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
<EditorReact <EditorReact
language={"sql"} language={"sql"}
content={query} content={query}

View File

@@ -166,7 +166,7 @@ export const OnExecuteQueryClick = async (): Promise<void> => {
export const QueryDocumentsPerPage = async ( export const QueryDocumentsPerPage = async (
firstItemIndex: number, firstItemIndex: number,
queryIterator: MinimalQueryIterator, queryIterator: MinimalQueryIterator
): Promise<void> => { ): Promise<void> => {
try { try {
useQueryCopilot.getState().setIsExecuting(true); useQueryCopilot.getState().setIsExecuting(true);
@@ -174,8 +174,7 @@ export const QueryDocumentsPerPage = async (
useTabs.getState().setIsQueryErrorThrown(false); useTabs.getState().setIsQueryErrorThrown(false);
const queryResults: QueryResults = await queryPagesUntilContentPresent( const queryResults: QueryResults = await queryPagesUntilContentPresent(
firstItemIndex, firstItemIndex,
async (firstItemIndex: number) => async (firstItemIndex: number) => queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex),
); );
useQueryCopilot.getState().setQueryResults(queryResults); useQueryCopilot.getState().setQueryResults(queryResults);
@@ -186,7 +185,7 @@ export const QueryDocumentsPerPage = async (
}); });
} catch (error) { } catch (error) {
const isCopilotActive = StringUtility.toBoolean( const isCopilotActive = StringUtility.toBoolean(
localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`), localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`)
); );
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, { traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,12 @@
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { useDialog } from "Explorer/Controls/Dialog";
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults"; import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import SplitterLayout from "react-splitter-layout"; import SplitterLayout from "react-splitter-layout";
import "react-splitter-layout/lib/index.css"; import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
@@ -84,7 +80,6 @@ interface IQueryTabStates {
isExecuting: boolean; isExecuting: boolean;
showCopilotSidebar: boolean; showCopilotSidebar: boolean;
queryCopilotGeneratedQuery: string; queryCopilotGeneratedQuery: string;
cancelQueryTimeoutID: NodeJS.Timeout;
} }
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> { export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
@@ -112,13 +107,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
isExecuting: false, isExecuting: false,
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar, showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
queryCopilotGeneratedQuery: useQueryCopilot.getState().query, queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
cancelQueryTimeoutID: undefined,
}; };
this.isCloseClicked = false; this.isCloseClicked = false;
this.splitterId = this.props.tabId + "_splitter"; this.splitterId = this.props.tabId + "_splitter";
this.queryEditorId = `queryeditor${this.props.tabId}`; this.queryEditorId = `queryeditor${this.props.tabId}`;
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB; this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId; this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
this.executeQueryButton = { this.executeQueryButton = {
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0, enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
visible: true, visible: true,
@@ -255,34 +250,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this.setState({ this.setState({
isExecuting: true, isExecuting: true,
}); });
let automaticallyCancelQueryAfterTimeout: boolean;
if (this.queryTimeoutEnabled()) {
const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout);
automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean(
StorageKey.AutomaticallyCancelQueryAfterTimeout,
);
const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => {
if (this.state.isExecuting) {
if (automaticallyCancelQueryAfterTimeout) {
this.queryAbortController.abort();
} else {
useDialog
.getState()
.showOkCancelModalDialog(
QueryConstants.CancelQueryTitle,
format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached),
"Yes",
() => this.queryAbortController.abort(),
"No",
undefined,
);
}
}
}, queryTimeout);
this.setState({
cancelQueryTimeoutID,
});
}
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
try { try {
@@ -306,14 +273,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
this.props.tabsBaseInstance.isExecuting(false); this.props.tabsBaseInstance.isExecuting(false);
this.setState({ this.setState({
isExecuting: false, isExecuting: false,
cancelQueryTimeoutID: undefined,
}); });
if (this.queryTimeoutEnabled()) {
clearTimeout(this.state.cancelQueryTimeoutID);
if (!automaticallyCancelQueryAfterTimeout) {
useDialog.getState().closeDialog();
}
}
this.togglesOnFocus(); this.togglesOnFocus();
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
@@ -445,10 +405,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
return this.state.sqlQueryEditorContent; return this.state.sqlQueryEditorContent;
} }
private queryTimeoutEnabled(): boolean {
return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled);
}
private unsubscribeCopilotSidebar: () => void; private unsubscribeCopilotSidebar: () => void;
componentDidMount(): void { componentDidMount(): void {

View File

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

View File

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

View File

@@ -7,6 +7,9 @@ import "../less/hostedexplorer.less";
import { AuthType } from "./AuthType"; import { AuthType } from "./AuthType";
import { DatabaseAccount } from "./Contracts/DataModels"; import { DatabaseAccount } from "./Contracts/DataModels";
import "./Explorer/Menus/NavBar/MeControlComponent.less"; import "./Explorer/Menus/NavBar/MeControlComponent.less";
import { useAADAuth } from "./hooks/useAADAuth";
import { useConfig } from "./hooks/useConfig";
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher"; import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer"; import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
@@ -17,9 +20,6 @@ import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
import "./Platform/Hosted/ConnectScreen.less"; import "./Platform/Hosted/ConnectScreen.less";
import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils"; import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils";
import "./Shared/appInsights"; import "./Shared/appInsights";
import { useAADAuth } from "./hooks/useAADAuth";
import { useConfig } from "./hooks/useConfig";
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
initializeIcons(); initializeIcons();
@@ -51,7 +51,6 @@ const App: React.FunctionComponent = () => {
authType: AuthType.AAD, authType: AuthType.AAD,
databaseAccount, databaseAccount,
authorizationToken: armToken, authorizationToken: armToken,
graphAuthorizationToken: graphToken
}; };
} else if (authType === AuthType.EncryptedToken) { } else if (authType === AuthType.EncryptedToken) {
frameWindow.hostedConfig = { frameWindow.hostedConfig = {

View File

@@ -10,7 +10,6 @@ export interface AAD {
authType: AuthType.AAD; authType: AuthType.AAD;
databaseAccount: DatabaseAccount; databaseAccount: DatabaseAccount;
authorizationToken: string; authorizationToken: string;
graphAuthorizationToken: string;
} }
export interface ConnectionString { export interface ConnectionString {

View File

@@ -1,13 +1,13 @@
// CSS Dependencies // CSS Dependencies
import { initializeIcons, loadTheme } from "@fluentui/react"; import { initializeIcons, loadTheme } from "@fluentui/react";
import "bootstrap/dist/css/bootstrap.css";
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel"; import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial"; import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial"; import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
import { userContext } from "UserContext";
import "bootstrap/dist/css/bootstrap.css";
import { useCarousel } from "hooks/useCarousel"; import { useCarousel } from "hooks/useCarousel";
import React, { useState } from "react"; import React, { useState } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { userContext } from "UserContext";
import "../externals/jquery-ui.min.css"; import "../externals/jquery-ui.min.css";
import "../externals/jquery-ui.min.js"; import "../externals/jquery-ui.min.js";
import "../externals/jquery-ui.structure.min.css"; import "../externals/jquery-ui.structure.min.css";
@@ -16,27 +16,27 @@ import "../externals/jquery.dataTables.min.css";
import "../externals/jquery.typeahead.min.css"; import "../externals/jquery.typeahead.min.css";
import "../externals/jquery.typeahead.min.js"; import "../externals/jquery.typeahead.min.js";
// Image Dependencies // Image Dependencies
import { Platform } from "ConfigContext";
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel"; import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import "../images/favicon.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import "../images/favicon.ico";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less";
import * as StyleConstants from "./Common/StyleConstants";
import { configContext, Platform } from "ConfigContext";
import "../less/documentDB.less"; import "../less/documentDB.less";
import "../less/forms.less"; import "../less/forms.less";
import "../less/infobox.less"; import "../less/infobox.less";
import "../less/menus.less"; import "../less/menus.less";
import "../less/messagebox.less"; import "../less/messagebox.less";
import "../less/resourceTree.less"; import "../less/resourceTree.less";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less";
import "../less/tree.less"; import "../less/tree.less";
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree"; import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer"; import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
import * as StyleConstants from "./Common/StyleConstants";
import "./Explorer/Controls/Accordion/AccordionComponent.less"; import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import { Dialog } from "./Explorer/Controls/Dialog"; import { Dialog } from "./Explorer/Controls/Dialog";
@@ -55,11 +55,11 @@ import "./Explorer/Panes/PanelComponent.less";
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent"; import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
import "./Explorer/SplashScreen/SplashScreen.less"; import "./Explorer/SplashScreen/SplashScreen.less";
import { Tabs } from "./Explorer/Tabs/Tabs"; import { Tabs } from "./Explorer/Tabs/Tabs";
import "./Libs/jquery";
import "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import "./Libs/jquery";
import { appThemeFabric } from "./Platform/Fabric/FabricTheme"; import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
import "./Shared/appInsights";
initializeIcons(); initializeIcons();
@@ -72,7 +72,6 @@ const App: React.FunctionComponent = () => {
const config = useConfig(); const config = useConfig();
if (config?.platform === Platform.Fabric) { if (config?.platform === Platform.Fabric) {
loadTheme(appThemeFabric); loadTheme(appThemeFabric);
import("../less/documentDBFabric.less");
} }
StyleConstants.updateStyles(); StyleConstants.updateStyles();
const explorer = useKnockoutExplorer(config?.platform); const explorer = useKnockoutExplorer(config?.platform);
@@ -92,6 +91,7 @@ const App: React.FunctionComponent = () => {
return ( return (
<div className="flexContainer" aria-hidden="false"> <div className="flexContainer" aria-hidden="false">
<LoadFabricOverrides />
<div id="divExplorer" className="flexContainer hideOverflows"> <div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div> <div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */} {/* Main Command Bar - Start */}
@@ -141,8 +141,20 @@ const App: React.FunctionComponent = () => {
); );
}; };
const mainElement = document.getElementById("Main"); ReactDOM.render(<App />, document.body);
ReactDOM.render(<App />, mainElement);
function LoadFabricOverrides(): JSX.Element {
if (configContext.platform === Platform.Fabric) {
const FabricStyle = React.lazy(() => import("./Platform/Fabric/FabricPlatform"));
return (
<React.Suspense fallback={<div></div>}>
<FabricStyle />
</React.Suspense>
);
} else {
return <></>;
}
}
function LoadingExplorer(): JSX.Element { function LoadingExplorer(): JSX.Element {
return ( return (

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,6 @@ export type Features = {
readonly enableCopilotFullSchema: boolean; readonly enableCopilotFullSchema: boolean;
readonly copilotChatFixedMonacoEditorHeight: boolean; readonly copilotChatFixedMonacoEditorHeight: boolean;
readonly enablePriorityBasedExecution: boolean; readonly enablePriorityBasedExecution: boolean;
readonly disableConnectionStringLogin: boolean;
// can be set via both flight and feature flag // can be set via both flight and feature flag
autoscaleDefault: boolean; autoscaleDefault: boolean;
@@ -115,7 +114,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"), enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"), copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"), enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
}; };
} }

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils"; import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@@ -48,7 +47,6 @@ export interface VCoreMongoConnectionParams {
} }
interface UserContext { interface UserContext {
readonly fabricDatabaseConnectionInfo?: FabricDatabaseConnectionInfo;
readonly authType?: AuthType; readonly authType?: AuthType;
readonly masterKey?: string; readonly masterKey?: string;
readonly subscriptionId?: string; readonly subscriptionId?: string;
@@ -79,7 +77,6 @@ interface UserContext {
collectionCreationDefaults: CollectionCreationDefaults; collectionCreationDefaults: CollectionCreationDefaults;
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString; sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams; readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
readonly accountRestrictedFromUser?: boolean;
} }
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo"; export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
@@ -172,4 +169,3 @@ function apiType(account: DatabaseAccount | undefined): ApiType {
} }
export { updateUserContext, userContext }; export { updateUserContext, userContext };

View File

@@ -60,27 +60,3 @@ export function getMsalInstance() {
const msalInstance = new msal.PublicClientApplication(config); const msalInstance = new msal.PublicClientApplication(config);
return msalInstance; return msalInstance;
} }
export async function isAccountRestrictedFromUser(accountName: string, graphToken: string): Promise<boolean> {
const checkUserAccessUrl: string = "https://localhost:12901/api/guest/accountrestrictions/accountrestrictedfromuser";
// const authorizationHeader = getAuthorizationHeader();
try {
const response: Response = await fetch(checkUserAccessUrl, {
method: "POST",
body: JSON.stringify({
accountName
}),
headers: {
// [authorizationHeader.header]: authorizationHeader.token,
[Constants.HttpHeaders.graphAuthorization]: graphToken,
[Constants.HttpHeaders.contentType]: "application/json",
}
});
const responseText: string = await response.text();
return responseText.toLowerCase() === "true";
} catch (e) {
console.log(e);
throw new Error(e);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,16 @@
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { FabricDatabaseConnectionInfo, FabricMessage } from "Contracts/FabricContract"; import { FabricMessage } from "Contracts/FabricContract";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { import { fetchEncryptedToken } from "Platform/Hosted/Components/ConnectExplorer";
handleRequestDatabaseResourceTokensResponse,
scheduleRefreshDatabaseResourceToken,
} from "Platform/Fabric/FabricUtil";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { fetchAccessData } from "hooks/usePortalAccessToken";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { AccountKind, Flights } from "../Common/Constants"; import { AccountKind, Flights } from "../Common/Constants";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { handleCachedDataMessage, sendMessage, sendReadyMessage } from "../Common/MessageHandler"; import { sendMessage, sendReadyMessage } from "../Common/MessageHandler";
import { Platform, configContext, updateConfigContext } from "../ConfigContext"; import { Platform, configContext, updateConfigContext } from "../ConfigContext";
import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts"; import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
@@ -36,7 +34,7 @@ import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { CollectionCreation } from "../Shared/Constants"; import { CollectionCreation } from "../Shared/Constants";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext"; import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
import { getAuthorizationHeader, getMsalInstance, isAccountRestrictedFromUser } from "../Utils/AuthorizationUtils"; import { getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils";
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation"; import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types"; import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
@@ -102,37 +100,70 @@ async function configureFabric(): Promise<Explorer> {
} }
const data: FabricMessage = event.data?.data; const data: FabricMessage = event.data?.data;
if (!data) { if (!data) {
return; return;
} }
switch (data.type) { switch (data.type) {
case "initialize": { case "initialize": {
const fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo = { // TODO For now, retrieve info from session storage. Replace with info injected into Data Explorer
endpoint: data.message.endpoint, const connectionString = data.connectionString ?? sessionStorage.getItem("connectionString");
databaseId: data.message.databaseId, if (!connectionString) {
resourceTokens: data.message.resourceTokens as { [resourceId: string]: string }, console.error("No connection string found in session storage");
resourceTokensTimestamp: data.message.resourceTokensTimestamp, return undefined;
}; }
explorer = await createExplorerFabric(fabricDatabaseConnectionInfo); const encryptedToken = await fetchEncryptedToken(connectionString);
resolve(explorer); // TODO Duplicated from useTokenMetadata
const encryptedTokenMetadata = await fetchAccessData(encryptedToken);
explorer.refreshAllDatabases().then(() => { const hostedConfig: EncryptedToken = {
openFirstContainer(explorer, fabricDatabaseConnectionInfo.databaseId); authType: AuthType.EncryptedToken,
}); encryptedToken,
scheduleRefreshDatabaseResourceToken(); encryptedTokenMetadata,
};
explorer = await configureWithEncryptedToken(hostedConfig);
resolve(explorer);
break; break;
} }
case "newContainer": case "newContainer":
explorer.onNewCollectionClicked(); explorer.onNewCollectionClicked();
break; break;
case "authorizationToken": { case "openTab": {
handleCachedDataMessage(data); // Expand database first
break; const databaseName = sessionStorage.getItem("openDatabaseName") ?? data.databaseName;
} const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
case "allResourceTokens": { if (database) {
// TODO call handleCachedDataMessage when Fabric echoes message id back await database.expandDatabase();
handleRequestDatabaseResourceTokensResponse(explorer, data.message as FabricDatabaseConnectionInfo); useDatabases.getState().updateDatabase(database);
useSelectedNode.getState().setSelectedNode(database);
let collectionResourceId = data.collectionName;
if (collectionResourceId === undefined) {
// Pick first collection if collectionName not specified in message
collectionResourceId = database.collections()[0]?.id();
}
if (collectionResourceId !== undefined) {
// Expand collection
const collection = database.collections().find((coll) => coll.id() === collectionResourceId);
collection.expandCollection();
useSelectedNode.getState().setSelectedNode(collection);
handleOpenAction(
{
actionType: ActionType.OpenCollectionTab,
databaseResourceId: databaseName,
collectionResourceId: data.collectionName,
tabKind: TabKind.SQLDocuments,
} as DataExplorerAction,
useDatabases.getState().databases,
explorer,
);
}
}
break; break;
} }
default: default:
@@ -147,41 +178,6 @@ async function configureFabric(): Promise<Explorer> {
}); });
} }
const openFirstContainer = async (explorer: Explorer, databaseName: string, collectionName?: string) => {
// Expand database first
databaseName = sessionStorage.getItem("openDatabaseName") ?? databaseName;
const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
if (database) {
await database.expandDatabase();
useDatabases.getState().updateDatabase(database);
useSelectedNode.getState().setSelectedNode(database);
let collectionResourceId = collectionName;
if (collectionResourceId === undefined) {
// Pick first collection if collectionName not specified in message
collectionResourceId = database.collections()[0]?.id();
}
if (collectionResourceId !== undefined) {
// Expand collection
const collection = database.collections().find((coll) => coll.id() === collectionResourceId);
collection.expandCollection();
useSelectedNode.getState().setSelectedNode(collection);
handleOpenAction(
{
actionType: ActionType.OpenCollectionTab,
databaseResourceId: databaseName,
collectionResourceId: collectionName,
tabKind: TabKind.SQLDocuments,
} as DataExplorerAction,
useDatabases.getState().databases,
explorer,
);
}
}
};
async function configureHosted(): Promise<Explorer> { async function configureHosted(): Promise<Explorer> {
const win = window as unknown as HostedExplorerChildFrame; const win = window as unknown as HostedExplorerChildFrame;
let explorer: Explorer; let explorer: Explorer;
@@ -227,11 +223,9 @@ async function configureHosted(): Promise<Explorer> {
async function configureHostedWithAAD(config: AAD): Promise<Explorer> { async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
// TODO: Refactor. updateUserContext needs to be called twice because listKeys below depends on userContext.authorizationToken // TODO: Refactor. updateUserContext needs to be called twice because listKeys below depends on userContext.authorizationToken
const accountRestrictedFromUser: boolean = await isAccountRestrictedFromUser(config.databaseAccount.name, config.graphAuthorizationToken);
updateUserContext({ updateUserContext({
authType: AuthType.AAD, authType: AuthType.AAD,
authorizationToken: `Bearer ${config.authorizationToken}`, authorizationToken: `Bearer ${config.authorizationToken}`,
accountRestrictedFromUser
}); });
const account = config.databaseAccount; const account = config.databaseAccount;
const accountResourceId = account.id; const accountResourceId = account.id;
@@ -321,25 +315,6 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
return explorer; return explorer;
} }
function createExplorerFabric(fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo): Explorer {
updateUserContext({
fabricDatabaseConnectionInfo,
authType: AuthType.ConnectionString,
databaseAccount: {
id: "",
location: "",
type: "",
name: "Mounted",
kind: AccountKind.Default,
properties: {
documentEndpoint: fabricDatabaseConnectionInfo.endpoint,
},
},
});
const explorer = new Explorer();
return explorer;
}
function configureWithEncryptedToken(config: EncryptedToken): Explorer { function configureWithEncryptedToken(config: EncryptedToken): Explorer {
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
updateUserContext({ updateUserContext({

View File

@@ -128,7 +128,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }), set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ showFeedbackModal: false }), closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }),
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
set({ hideFeedbackModalForLikedQueries }), set({ hideFeedbackModalForLikedQueries }),
refreshCorrelationId: () => set({ correlationId: guid() }), refreshCorrelationId: () => set({ correlationId: guid() }),

View File

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