Compare commits

..

4 Commits

Author SHA1 Message Date
Tanuj Mittal
f788967ef2 Disable endtoendpuppeteer tests 2020-09-25 14:27:05 -07:00
Tanuj Mittal
823cbc4229 Do not fail when trying to find DE window with cross origin 2020-09-25 13:49:46 -07:00
Steve Faulkner
a04afc48e3 Fix hotfix syntax in workflow 2020-09-21 16:06:40 -05:00
artrejo
b86551c784 Remove AFEC check for Synapse Link and Mongo 2020-09-21 13:58:27 -07:00
55 changed files with 575 additions and 831 deletions

View File

@@ -5,7 +5,6 @@ module.exports = {
headless: isCI, headless: isCI,
slowMo: 50, slowMo: 50,
defaultViewport: null, defaultViewport: null,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true
args: ["--disable-web-security"]
} }
}; };

View File

@@ -1743,7 +1743,7 @@ input::-webkit-calendar-picker-indicator {
padding-right: 34px; padding-right: 34px;
color: @BaseDark; color: @BaseDark;
overflow-y: auto; overflow-y: auto;
overflow-x: auto; overflow-x: hidden;
margin: (2 * @MediumSpace) 0px; margin: (2 * @MediumSpace) 0px;
} }
@@ -2078,7 +2078,7 @@ a:link {
.resourceTreeAndTabs { .resourceTreeAndTabs {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
overflow: auto; overflow: hidden;
height: 100%; height: 100%;
} }

View File

@@ -170,8 +170,89 @@ export enum MongoBackendEndpointType {
remote remote
} }
export class MongoBackend {
public static localhostEndpoint: string = "/api/mongo/explorer";
public static centralUsEndpoint: string = "https://main.documentdb.ext.azure.com/api/mongo/explorer";
public static northEuropeEndpoint: string = "https://main.documentdb.ext.azure.com/api/mongo/explorer";
public static southEastAsiaEndpoint: string = "https://main.documentdb.ext.azure.com/api/mongo/explorer";
public static endpointsByRegion: any = {
default: MongoBackend.centralUsEndpoint,
northeurope: MongoBackend.northEuropeEndpoint,
ukwest: MongoBackend.northEuropeEndpoint,
uksouth: MongoBackend.northEuropeEndpoint,
westeurope: MongoBackend.northEuropeEndpoint,
australiaeast: MongoBackend.southEastAsiaEndpoint,
australiasoutheast: MongoBackend.southEastAsiaEndpoint,
centralindia: MongoBackend.southEastAsiaEndpoint,
eastasia: MongoBackend.southEastAsiaEndpoint,
japaneast: MongoBackend.southEastAsiaEndpoint,
japanwest: MongoBackend.southEastAsiaEndpoint,
koreacentral: MongoBackend.southEastAsiaEndpoint,
koreasouth: MongoBackend.southEastAsiaEndpoint,
southeastasia: MongoBackend.southEastAsiaEndpoint,
southindia: MongoBackend.southEastAsiaEndpoint,
westindia: MongoBackend.southEastAsiaEndpoint
};
public static endpointsByEnvironment: any = {
default: MongoBackendEndpointType.local,
localhost: MongoBackendEndpointType.local,
prod1: MongoBackendEndpointType.remote,
prod2: MongoBackendEndpointType.remote
};
}
// TODO: 435619 Add default endpoints per cloud and use regional only when available // TODO: 435619 Add default endpoints per cloud and use regional only when available
export class CassandraBackend { export class CassandraBackend {
public static readonly localhostEndpoint: string = "https://localhost:12901/";
public static readonly devEndpoint: string = "https://platformproxycassandradev.azurewebsites.net/";
public static readonly centralUsEndpoint: string = "https://main.documentdb.ext.azure.com/";
public static readonly northEuropeEndpoint: string = "https://main.documentdb.ext.azure.com/";
public static readonly southEastAsiaEndpoint: string = "https://main.documentdb.ext.azure.com/";
public static readonly bf_default: string = "https://main.documentdb.ext.microsoftazure.de/";
public static readonly mc_default: string = "https://main.documentdb.ext.azure.cn/";
public static readonly ff_default: string = "https://main.documentdb.ext.azure.us/";
public static readonly endpointsByRegion: any = {
default: CassandraBackend.centralUsEndpoint,
northeurope: CassandraBackend.northEuropeEndpoint,
ukwest: CassandraBackend.northEuropeEndpoint,
uksouth: CassandraBackend.northEuropeEndpoint,
westeurope: CassandraBackend.northEuropeEndpoint,
australiaeast: CassandraBackend.southEastAsiaEndpoint,
australiasoutheast: CassandraBackend.southEastAsiaEndpoint,
centralindia: CassandraBackend.southEastAsiaEndpoint,
eastasia: CassandraBackend.southEastAsiaEndpoint,
japaneast: CassandraBackend.southEastAsiaEndpoint,
japanwest: CassandraBackend.southEastAsiaEndpoint,
koreacentral: CassandraBackend.southEastAsiaEndpoint,
koreasouth: CassandraBackend.southEastAsiaEndpoint,
southeastasia: CassandraBackend.southEastAsiaEndpoint,
southindia: CassandraBackend.southEastAsiaEndpoint,
westindia: CassandraBackend.southEastAsiaEndpoint,
// Black Forest
germanycentral: CassandraBackend.bf_default,
germanynortheast: CassandraBackend.bf_default,
// Fairfax
usdodeast: CassandraBackend.ff_default,
usdodcentral: CassandraBackend.ff_default,
usgovarizona: CassandraBackend.ff_default,
usgoviowa: CassandraBackend.ff_default,
usgovtexas: CassandraBackend.ff_default,
usgovvirginia: CassandraBackend.ff_default,
// Mooncake
chinaeast: CassandraBackend.mc_default,
chinaeast2: CassandraBackend.mc_default,
chinanorth: CassandraBackend.mc_default,
chinanorth2: CassandraBackend.mc_default
};
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete"; public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete"; public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
public static readonly queryApi: string = "api/cassandra"; public static readonly queryApi: string = "api/cassandra";
@@ -481,11 +562,3 @@ export class AnalyticalStorageTtl {
public static readonly Infinite: number = -1; public static readonly Infinite: number = -1;
public static readonly Disabled: number = 0; public static readonly Disabled: number = 0;
} }
export class TerminalQueryParams {
public static readonly Terminal = "terminal";
public static readonly Server = "server";
public static readonly Token = "token";
public static readonly SubscriptionId = "subscriptionId";
public static readonly TerminalEndpoint = "terminalEndpoint";
}

View File

@@ -1,8 +1,49 @@
import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels";
import { AuthType } from "../AuthType";
import { StringUtils } from "../Utils/StringUtils";
import Explorer from "../Explorer/Explorer";
export default class EnvironmentUtility { export default class EnvironmentUtility {
public static getMongoBackendEndpoint(serverId: string, location: string, extensionEndpoint: string = ""): string {
const defaultEnvironment: string = "default";
const defaultLocation: string = "default";
let environment: string = serverId;
const endpointType: Constants.MongoBackendEndpointType =
Constants.MongoBackend.endpointsByEnvironment[environment] ||
Constants.MongoBackend.endpointsByEnvironment[defaultEnvironment];
if (endpointType === Constants.MongoBackendEndpointType.local) {
return `${extensionEndpoint}${Constants.MongoBackend.localhostEndpoint}`;
}
const normalizedLocation = EnvironmentUtility.normalizeRegionName(location);
return (
Constants.MongoBackend.endpointsByRegion[normalizedLocation] ||
Constants.MongoBackend.endpointsByRegion[defaultLocation]
);
}
public static isAadUser(): boolean {
return window.authType === AuthType.AAD;
}
public static getCassandraBackendEndpoint(explorer: Explorer): string {
const defaultLocation: string = "default";
const location: string = EnvironmentUtility.normalizeRegionName(explorer.databaseAccount().location);
return (
Constants.CassandraBackend.endpointsByRegion[location] ||
Constants.CassandraBackend.endpointsByRegion[defaultLocation]
);
}
public static normalizeArmEndpointUri(uri: string): string { public static normalizeArmEndpointUri(uri: string): string {
if (uri && uri.slice(-1) !== "/") { if (uri && uri.slice(-1) !== "/") {
return `${uri}/`; return `${uri}/`;
} }
return uri; return uri;
} }
private static normalizeRegionName(region: string): string {
return region && StringUtils.stripSpacesFromString(region.toLocaleLowerCase());
}
} }

View File

@@ -25,4 +25,34 @@ describe("Message Handler", () => {
MessageHandler.runGarbageCollector(); MessageHandler.runGarbageCollector();
expect(MessageHandler.RequestMap["123"]).toBeUndefined(); expect(MessageHandler.RequestMap["123"]).toBeUndefined();
}); });
describe("getDataExplorerWindow", () => {
it("should return current window if current window has dataExplorerPlatform property", () => {
const currentWindow: Window = { dataExplorerPlatform: 0 } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toEqual(currentWindow);
});
it("should return current window's parent if current window's parent has dataExplorerPlatform property", () => {
const parentWindow: Window = { dataExplorerPlatform: 0 } as any;
const currentWindow: Window = { parent: parentWindow } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toEqual(parentWindow);
});
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is reference to itself", () => {
const parentWindow: Window = {} as any;
(parentWindow as any).parent = parentWindow; // If a window does not have a parent, its parent property is a reference to itself.
const currentWindow: Window = { parent: parentWindow } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toBeUndefined();
});
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is not defined", () => {
const parentWindow: Window = {} as any;
const currentWindow: Window = { parent: parentWindow } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toBeUndefined();
});
});
}); });

View File

@@ -2,7 +2,6 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
import Q from "q"; import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "./Constants"; import * as Constants from "./Constants";
import { getDataExplorerWindow } from "../Utils/WindowUtils";
export interface CachedDataPromise<T> { export interface CachedDataPromise<T> {
deferred: Q.Deferred<T>; deferred: Q.Deferred<T>;
@@ -61,6 +60,31 @@ export function sendMessage(data: any): void {
} }
} }
// Only exported for unit tests
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
// Start with the current window and traverse up the parent hierarchy to find a window
// with `dataExplorerPlatform` property
let dataExplorerWindow: Window | undefined = currentWindow;
try {
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
// eslint-disable-next-line @typescript-eslint/no-explicit-any
while (dataExplorerWindow && (dataExplorerWindow as any).dataExplorerPlatform == undefined) {
// If a window does not have a parent, its parent property is a reference to itself.
if (dataExplorerWindow.parent == dataExplorerWindow) {
dataExplorerWindow = undefined;
} else {
dataExplorerWindow = dataExplorerWindow.parent;
}
}
} catch (error) {
// This can happen if we come across parent from a different origin
dataExplorerWindow = undefined;
}
return dataExplorerWindow;
};
export function canSendMessage(): boolean { export function canSendMessage(): boolean {
return window.parent !== window; return window.parent !== window;
} }

View File

@@ -1,8 +1,9 @@
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { resetConfigContext, updateConfigContext } from "../ConfigContext"; import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels"; import { DatabaseAccount } from "../Contracts/DataModels";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
jest.mock("../ResourceProvider/ResourceProviderClient.ts"); jest.mock("../ResourceProvider/ResourceProviderClient.ts");
@@ -236,19 +237,19 @@ describe("MongoProxyClient", () => {
}); });
it("returns a production endpoint", () => { it("returns a production endpoint", () => {
const endpoint = getEndpoint(); const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer"); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
}); });
it("returns a development endpoint", () => { it("returns a development endpoint", () => {
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" }); updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
const endpoint = getEndpoint(); const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer"); expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
}); });
it("returns a guest endpoint", () => { it("returns a guest endpoint", () => {
window.authType = AuthType.EncryptedToken; window.authType = AuthType.EncryptedToken;
const endpoint = getEndpoint(); const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
}); });
}); });

View File

@@ -10,6 +10,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants"; import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import EnvironmentUtility from "./EnvironmentUtility";
import { MinimalQueryIterator } from "./IteratorUtilities"; import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
@@ -77,7 +78,7 @@ export function queryDocuments(
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : "" collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
}; };
const endpoint = getEndpoint() || ""; const endpoint = getEndpoint(databaseAccount) || "";
const headers = { const headers = {
...defaultHeaders, ...defaultHeaders,
@@ -138,7 +139,7 @@ export function readDocument(
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
}; };
const endpoint = getEndpoint(); const endpoint = getEndpoint(databaseAccount);
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
method: "GET", method: "GET",
@@ -178,7 +179,7 @@ export function createDocument(
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "" pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
}; };
const endpoint = getEndpoint(); const endpoint = getEndpoint(databaseAccount);
return window return window
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, { .fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
@@ -220,7 +221,7 @@ export function updateDocument(
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
}; };
const endpoint = getEndpoint(); const endpoint = getEndpoint(databaseAccount);
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
@@ -259,7 +260,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
pk: pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : "" documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
}; };
const endpoint = getEndpoint(); const endpoint = getEndpoint(databaseAccount);
return window return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, { .fetch(`${endpoint}?${queryString.stringify(params)}`, {
@@ -302,7 +303,7 @@ export function createMongoCollectionWithProxy(
autoPilotThroughput: params.autoPilotMaxThroughput?.toString() autoPilotThroughput: params.autoPilotMaxThroughput?.toString()
}; };
const endpoint = getEndpoint(); const endpoint = getEndpoint(databaseAccount);
return window return window
.fetch( .fetch(
@@ -326,9 +327,12 @@ export function createMongoCollectionWithProxy(
}); });
} }
export function getEndpoint(): string { export function getEndpoint(databaseAccount: DataModels.DatabaseAccount): string {
const serverId = window.dataExplorer.serverId();
const extensionEndpoint = window.dataExplorer.extensionEndpoint(); const extensionEndpoint = window.dataExplorer.extensionEndpoint();
let url = (configContext.MONGO_BACKEND_ENDPOINT || extensionEndpoint) + "/api/mongo/explorer"; let url = configContext.MONGO_BACKEND_ENDPOINT
? configContext.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer"
: EnvironmentUtility.getMongoBackendEndpoint(serverId, databaseAccount.location, extensionEndpoint);
if (window.authType === AuthType.EncryptedToken) { if (window.authType === AuthType.EncryptedToken) {
url = url.replace("api/mongo", "api/guest/mongo"); url = url.replace("api/mongo", "api/guest/mongo");

View File

@@ -1,87 +0,0 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { readOffers } from "./readOffers";
import { userContext } from "../../UserContext";
export const readDatabaseOffer = async (
params: DataModels.ReadDatabaseOfferParams
): Promise<DataModels.OfferWithHeaders> => {
let offerId = params.offerId;
if (!offerId) {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
try {
offerId = await getDatabaseOfferIdWithARM(params.databaseId);
} catch (error) {
if (error.code !== "NotFound") {
throw new Error(error);
}
return undefined;
}
} else {
offerId = await getDatabaseOfferIdWithSDK(params.databaseResourceId);
if (!offerId) {
return undefined;
}
}
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
const response = await client()
.offer(offerId)
.read(options);
return (
response && {
...response.resource,
headers: response.headers
}
);
};
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
let rpResponse;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await getSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await getMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await getCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await getGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.name;
};
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
const offers = await readOffers();
const offer = offers.find(offer => offer.resource === databaseResourceId);
return offer?.id;
};

View File

@@ -1,32 +0,0 @@
import { Offer } from "../../Contracts/DataModels";
import { ClientDefaults } from "../Constants";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { Platform, configContext } from "../../ConfigContext";
import { client } from "../CosmosClient";
import { sendCachedDataMessage } from "../MessageHandler";
import { userContext } from "../../UserContext";
export const readOffers = async (): Promise<Offer[]> => {
try {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
userContext.databaseAccount.id,
ClientDefaults.portalCacheTimeoutMs
]);
}
} catch (error) {
// If error getting cached Offers, continue on and read via SDK
}
return client()
.offers.readAll()
.fetchAll()
.then(response => response.resources)
.catch(error => {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
return [];
}
throw error;
});
};

View File

@@ -6,7 +6,7 @@ export enum Platform {
interface ConfigContext { interface ConfigContext {
platform: Platform; platform: Platform;
allowedParentFrameOrigins: string[]; allowedParentFrameOrigins: RegExp;
gitSha?: string; gitSha?: string;
proxyPath?: string; proxyPath?: string;
AAD_ENDPOINT: string; AAD_ENDPOINT: string;
@@ -30,12 +30,7 @@ interface ConfigContext {
// Default configuration // Default configuration
let configContext: Readonly<ConfigContext> = { let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal, platform: Platform.Portal,
allowedParentFrameOrigins: [ allowedParentFrameOrigins: /^https:\/\/portal\.azure\.com$|^https:\/\/portal\.azure\.us$|^https:\/\/portal\.azure\.cn$|^https:\/\/portal\.microsoftazure\.de$|^https:\/\/.+\.portal\.azure\.com$|^https:\/\/.+\.portal\.azure\.us$|^https:\/\/.+\.portal\.azure\.cn$|^https:\/\/.+\.portal\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.com$|^https:\/\/main\.documentdb\.ext\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.cn$|^https:\/\/main\.documentdb\.ext\.azure\.us$/,
`^https:\\/\\/cosmos.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+.portal.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+.ext.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+microsoftazure.de$`
],
// 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/",
@@ -78,13 +73,8 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
const response = await fetch("./config.json"); const response = await fetch("./config.json");
if (response.status === 200) { if (response.status === 200) {
try { try {
const { allowedParentFrameOrigins, ...externalConfig } = await response.json(); const externalConfig = await response.json();
Object.assign(configContext, externalConfig); Object.assign(configContext, externalConfig);
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
updateConfigContext({
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins]
});
}
} catch (error) { } catch (error) {
console.error("Unable to parse json in config file"); console.error("Unable to parse json in config file");
console.error(error); console.error(error);

View File

@@ -289,12 +289,6 @@ export interface CreateCollectionParams {
uniqueKeyPolicy?: UniqueKeyPolicy; uniqueKeyPolicy?: UniqueKeyPolicy;
} }
export interface ReadDatabaseOfferParams {
databaseId: string;
databaseResourceId?: string;
offerId?: string;
}
export interface Notification { export interface Notification {
id: string; id: string;
kind: string; kind: string;

View File

@@ -81,15 +81,15 @@ export interface Database extends TreeNode {
selectedSubnodeKind: ko.Observable<CollectionTabKind>; selectedSubnodeKind: ko.Observable<CollectionTabKind>;
selectDatabase(): void; selectDatabase(): void;
expandDatabase(): Promise<void>; expandDatabase(): void;
collapseDatabase(): void; collapseDatabase(): void;
loadCollections(): Promise<void>; loadCollections(): Q.Promise<void>;
findCollectionWithId(collectionRid: string): Collection; findCollectionWithId(collectionRid: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void; openAddCollection(database: Database, event: MouseEvent): void;
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void; onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
readSettings(): void;
onSettingsClick: () => void; onSettingsClick: () => void;
loadOffer(): Promise<void>;
} }
export interface CollectionBase extends TreeNode { export interface CollectionBase extends TreeNode {

View File

@@ -42,8 +42,7 @@ export class ResourceTreeContextMenuButtonFactory {
const deleteDatabaseMenuItem = { const deleteDatabaseMenuItem = {
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: () => container.deleteDatabaseConfirmationPane.open(), onClick: () => container.deleteDatabaseConfirmationPane.open(),
label: container.deleteDatabaseText(), label: container.deleteDatabaseText()
styleClass: "deleteDatabaseMenuItem"
}; };
return [newCollectionMenuItem, deleteDatabaseMenuItem]; return [newCollectionMenuItem, deleteDatabaseMenuItem];
} }
@@ -113,8 +112,7 @@ export class ResourceTreeContextMenuButtonFactory {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null); selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
}, },
label: container.deleteCollectionText(), label: container.deleteCollectionText()
styleClass: "deleteCollectionMenuItem"
}); });
return items; return items;

View File

@@ -86,7 +86,6 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel {
public onRemoveItemKeyPress = (data: any, event: KeyboardEvent, source: any): boolean => { public onRemoveItemKeyPress = (data: any, event: KeyboardEvent, source: any): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.removeItem(data, event); this.removeItem(data, event);
(document.querySelector(".dynamicListItem:last-of-type input") as HTMLElement).focus();
event.stopPropagation(); event.stopPropagation();
return false; return false;
} }
@@ -95,7 +94,7 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel {
public addItem(): void { public addItem(): void {
this.listItems.push({ value: ko.observable("") }); this.listItems.push({ value: ko.observable("") });
(document.querySelector(".dynamicListItem:last-of-type input") as HTMLElement).focus(); document.getElementById("uniqueKeyItems").focus();
} }
public onAddItemKeyPress = (source: any, event: KeyboardEvent): boolean => { public onAddItemKeyPress = (source: any, event: KeyboardEvent): boolean => {

View File

@@ -8,8 +8,6 @@ import * as Logger from "../../../Common/Logger";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { StringUtils } from "../../../Utils/StringUtils"; import { StringUtils } from "../../../Utils/StringUtils";
import { userContext } from "../../../UserContext";
import { TerminalQueryParams } from "../../../Common/Constants";
export interface NotebookTerminalComponentProps { export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
@@ -34,11 +32,11 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
public getTerminalParams(): Map<string, string> { public getTerminalParams(): Map<string, string> {
let params: Map<string, string> = new Map<string, string>(); let params: Map<string, string> = new Map<string, string>();
params.set(TerminalQueryParams.Terminal, "true"); params.set("terminal", "true");
const terminalEndpoint: string = this.tryGetTerminalEndpoint(); const terminalEndpoint: string = this.tryGetTerminalEndpoint();
if (terminalEndpoint) { if (terminalEndpoint) {
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint); params.set("terminalEndpoint", terminalEndpoint);
} }
return params; return params;
@@ -77,13 +75,11 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
return ""; return "";
} }
params.set(TerminalQueryParams.Server, serverInfo.notebookServerEndpoint); params.set("server", serverInfo.notebookServerEndpoint);
if (serverInfo.authToken && serverInfo.authToken.length > 0) { if (serverInfo.authToken && serverInfo.authToken.length > 0) {
params.set(TerminalQueryParams.Token, serverInfo.authToken); params.set("token", serverInfo.authToken);
} }
params.set(TerminalQueryParams.SubscriptionId, userContext.subscriptionId);
let result: string = "terminal.html?"; let result: string = "terminal.html?";
for (let key of params.keys()) { for (let key of params.keys()) {
result += `${key}=${encodeURIComponent(params.get(key))}&`; result += `${key}=${encodeURIComponent(params.get(key))}&`;

View File

@@ -30,7 +30,6 @@ export interface NotebookViewerComponentProps {
isFavorite?: boolean; isFavorite?: boolean;
backNavigationText: string; backNavigationText: string;
hideInputs?: boolean; hideInputs?: boolean;
hidePrompts?: boolean;
onBackClick: () => void; onBackClick: () => void;
onTagClick: (tag: string) => void; onTagClick: (tag: string) => void;
} }
@@ -149,8 +148,7 @@ export class NotebookViewerComponent extends React.Component<
{this.state.showProgressBar && <ProgressIndicator />} {this.state.showProgressBar && <ProgressIndicator />}
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, { {this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
hideInputs: this.props.hideInputs, hideInputs: this.props.hideInputs
hidePrompts: this.props.hidePrompts
})} })}
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />} {this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}

View File

@@ -159,20 +159,4 @@ describe("TreeNodeComponent", () => {
const wrapper = shallow(<TreeNodeComponent {...props} />); const wrapper = shallow(<TreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("renders loading icon", () => {
const node: TreeNode = {
label: "label",
children: [],
isExpanded: true
};
const props = {
node,
generation: 2,
paddingLeft: 9
};
const wrapper = shallow(<TreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
}); });

View File

@@ -17,14 +17,12 @@ import {
import TriangleDownIcon from "../../../../images/Triangle-down.svg"; import TriangleDownIcon from "../../../../images/Triangle-down.svg";
import TriangleRightIcon from "../../../../images/Triangle-right.svg"; import TriangleRightIcon from "../../../../images/Triangle-right.svg";
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
export interface TreeNodeMenuItem { export interface TreeNodeMenuItem {
label: string; label: string;
onClick: () => void; onClick: () => void;
iconSrc?: string; iconSrc?: string;
isDisabled?: boolean; isDisabled?: boolean;
styleClass?: string;
} }
export interface TreeNode { export interface TreeNode {
@@ -39,7 +37,6 @@ export interface TreeNode {
data?: any; // Piece of data corresponding to this node data?: any; // Piece of data corresponding to this node
timestamp?: number; timestamp?: number;
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean;
isSelected?: () => boolean; isSelected?: () => boolean;
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => void; onExpanded?: () => void;
@@ -186,9 +183,6 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
)} )}
{node.contextMenu && this.renderContextMenuButton(node)} {node.contextMenu && this.renderContextMenuButton(node)}
</div> </div>
<div className="loadingIconContainer">
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
</div>
{node.children && ( {node.children && (
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}> <AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
<div className="nodeChildren" data-test={node.label}> <div className="nodeChildren" data-test={node.label}>
@@ -262,20 +256,13 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())} onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
> >
{props.item.onRenderIcon()} {props.item.onRenderIcon()}
<span <span className="treeComponentMenuItemLabel">{props.item.text}</span>
className={
"treeComponentMenuItemLabel" + (props.item.className ? ` ${props.item.className}Label` : "")
}
>
{props.item.text}
</span>
</div> </div>
), ),
items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({ items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
key: menuItem.label, key: menuItem.label,
text: menuItem.label, text: menuItem.label,
disabled: menuItem.isDisabled, disabled: menuItem.isDisabled,
className: menuItem.styleClass,
onClick: menuItem.onClick, onClick: menuItem.onClick,
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" /> onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
})) }))

View File

@@ -63,15 +63,6 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
label label
</span> </span>
</div> </div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight <AnimateHeight
animateOpacity={false} animateOpacity={false}
animationStateClasses={ animationStateClasses={
@@ -188,7 +179,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
"isBeakVisible": false, "isBeakVisible": false,
"items": Array [ "items": Array [
Object { Object {
"className": undefined,
"disabled": true, "disabled": true,
"key": "menuLabel", "key": "menuLabel",
"onClick": undefined, "onClick": undefined,
@@ -211,15 +201,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
/> />
</div> </div>
</div> </div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight <AnimateHeight
animateOpacity={false} animateOpacity={false}
animationStateClasses={ animationStateClasses={
@@ -280,77 +261,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
</div> </div>
`; `;
exports[`TreeNodeComponent renders loading icon 1`] = `
<div
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
>
<div
className="treeNodeHeader "
data-test="label"
style={
Object {
"paddingLeft": 9,
}
}
tabIndex={-1}
>
<img
alt="label branch is expanded"
className="expandCollapseIcon"
onKeyPress={[Function]}
role="button"
src=""
tabIndex={0}
/>
<span
className="nodeLabel"
title="label"
>
label
</span>
</div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={
Object {
"animating": "rah-animating",
"animatingDown": "rah-animating--down",
"animatingToHeightAuto": "rah-animating--to-height-auto",
"animatingToHeightSpecific": "rah-animating--to-height-specific",
"animatingToHeightZero": "rah-animating--to-height-zero",
"animatingUp": "rah-animating--up",
"static": "rah-static",
"staticHeightAuto": "rah-static--height-auto",
"staticHeightSpecific": "rah-static--height-specific",
"staticHeightZero": "rah-static--height-zero",
}
}
applyInlineTransitions={true}
delay={0}
duration={200}
easing="ease"
height="auto"
style={Object {}}
>
<div
className="nodeChildren"
data-test="label"
/>
</AnimateHeight>
</div>
`;
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = ` exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
<div <div
className="nodeClassname main12 nodeItem " className="nodeClassname main12 nodeItem "
@@ -421,15 +331,6 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
/> />
</div> </div>
</div> </div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight <AnimateHeight
animateOpacity={false} animateOpacity={false}
animationStateClasses={ animationStateClasses={
@@ -549,15 +450,6 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
label label
</span> </span>
</div> </div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight <AnimateHeight
animateOpacity={false} animateOpacity={false}
animationStateClasses={ animationStateClasses={

View File

@@ -20,7 +20,7 @@
} }
&.showingMenu { &.showingMenu {
background-color: #eee; background-color: #EEE;
} }
.treeMenuEllipsis { .treeMenuEllipsis {
@@ -78,12 +78,3 @@
vertical-align: text-bottom; vertical-align: text-bottom;
} }
} }
.loadingIconContainer {
width: 100%;
.loadingIcon {
height: 6px;
margin-left: 38px;
}
}

View File

@@ -15,7 +15,7 @@ import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import Database from "./Tree/Database"; import Database from "./Tree/Database";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane"; import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { refreshCachedResources } from "../Common/DocumentClientUtilityBase"; import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { readCollection } from "../Common/dataAccess/readCollection"; import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
@@ -1424,40 +1424,71 @@ export default class Explorer {
// TODO: Refactor // TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer(); const deferred: Q.Deferred<any> = Q.defer();
this._setLoadingStatusText("Fetching databases...");
readDatabases().then( const refreshDatabases = (offers?: DataModels.Offer[]) => {
(databases: DataModels.Database[]) => { this._setLoadingStatusText("Fetching databases...");
this._setLoadingStatusText("Successfully fetched databases."); readDatabases().then(
TelemetryProcessor.traceSuccess( (databases: DataModels.Database[]) => {
Action.LoadDatabases, this._setLoadingStatusText("Successfully fetched databases.");
{ TelemetryProcessor.traceSuccess(
databaseAccountName: this.databaseAccount().name, Action.LoadDatabases,
defaultExperience: this.defaultExperience(), {
dataExplorerArea: Constants.Areas.ResourceTree databaseAccountName: this.databaseAccount().name,
}, defaultExperience: this.defaultExperience(),
startKey dataExplorerArea: Constants.Areas.ResourceTree
);
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
const deltaDatabases = this.getDeltaDatabases(databases);
this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
.then(
() => {
this._setLoadingStatusText("Successfully fetched containers.");
deferred.resolve();
}, },
reason => { startKey
this._setLoadingStatusText("Failed to fetch containers."); );
deferred.reject(reason); const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
} const deltaDatabases = this.getDeltaDatabases(databases, offers);
) this.addDatabasesToList(deltaDatabases.toAdd);
.finally(() => this.isRefreshingExplorer(false)); this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
.then(
() => {
this._setLoadingStatusText("Successfully fetched containers.");
deferred.resolve();
},
reason => {
this._setLoadingStatusText("Failed to fetch containers.");
deferred.reject(reason);
}
)
.finally(() => this.isRefreshingExplorer(false));
},
error => {
this._setLoadingStatusText("Failed to fetch databases.");
this.isRefreshingExplorer(false);
deferred.reject(error);
TelemetryProcessor.traceFailure(
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: JSON.stringify(error)
},
startKey
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while refreshing databases: ${JSON.stringify(error)}`
);
}
);
};
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() });
this._setLoadingStatusText("Fetching offers...");
offerPromise.then(
(offers: DataModels.Offer[]) => {
this._setLoadingStatusText("Successfully fetched offers.");
refreshDatabases(offers);
}, },
error => { error => {
this._setLoadingStatusText("Failed to fetch databases."); this._setLoadingStatusText("Failed to fetch offers.");
this.isRefreshingExplorer(false); this.isRefreshingExplorer(false);
deferred.reject(error); deferred.reject(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
@@ -2072,13 +2103,16 @@ export default class Explorer {
defaultExperience: this.defaultExperience && this.defaultExperience(), defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree dataExplorerArea: Constants.Areas.ResourceTree
}); });
databasesToLoad.forEach(async (database: ViewModels.Database) => { databasesToLoad.forEach((database: ViewModels.Database) => {
await database.loadCollections(); loadCollectionPromises.push(
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid); database.loadCollections().finally(() => {
if (isNewDatabase) { const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
database.expandDatabase(); if (isNewDatabase) {
} database.expandDatabase();
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid); }
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
})
);
}); });
Q.all(loadCollectionPromises).done( Q.all(loadCollectionPromises).done(
@@ -2223,7 +2257,8 @@ export default class Explorer {
} }
private getDeltaDatabases( private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[] updatedDatabaseList: DataModels.Database[],
updatedOffersList: DataModels.Offer[]
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } { ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } {
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some( const databaseExists = _.some(
@@ -2232,9 +2267,10 @@ export default class Explorer {
); );
return !databaseExists; return !databaseExists;
}); });
const databasesToAdd: ViewModels.Database[] = newDatabases.map( const databasesToAdd: ViewModels.Database[] = _.map(newDatabases, (newDatabase: DataModels.Database) => {
(newDatabase: DataModels.Database) => new Database(this, newDatabase) const databaseOffer: DataModels.Offer = this.getOfferForResource(updatedOffersList, newDatabase._self);
); return new Database(this, newDatabase, databaseOffer);
});
let databasesToDelete: ViewModels.Database[] = []; let databasesToDelete: ViewModels.Database[] = [];
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
@@ -2284,6 +2320,10 @@ export default class Explorer {
return null; return null;
} }
private getOfferForResource(offers: DataModels.Offer[], resourceId: string): DataModels.Offer {
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
}
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> { public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled"; const error = "Attempt to upload notebook, but notebook is not enabled";
@@ -3120,15 +3160,4 @@ export default class Explorer {
} }
} }
} }
public async loadSelectedDatabaseOffer(): Promise<void> {
const database = this.findSelectedDatabase();
await database?.loadOffer();
}
public async loadDatabaseOffers(): Promise<void> {
this.databases()?.forEach(async (database: ViewModels.Database) => {
await database.loadOffer();
});
}
} }

View File

@@ -391,6 +391,31 @@ export class CommandBarComponentButtonFactory {
return buttons; return buttons;
} }
private static createScaleAndSettingsButton(container: Explorer): CommandButtonComponentProps {
let isShared = false;
if (container.isDatabaseNodeSelected()) {
isShared = container.findSelectedDatabase().isDatabaseShared();
} else if (container.isNodeKindSelected("Collection")) {
const database: ViewModels.Database = container.findSelectedCollection().getDatabase();
isShared = database && database.isDatabaseShared();
}
const label = isShared ? "Settings" : "Scale & Settings";
return {
iconSrc: ScaleIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && (<any>selectedCollection).onSettingsClick();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected()
};
}
private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps { private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook"; const label = "New Notebook";
return { return {

View File

@@ -113,14 +113,11 @@ export default class NotebookManager {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
this.params.resourceTree.triggerRender(); this.params.resourceTree.triggerRender();
}); });
this.refreshPinnedRepos(); this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
} }
public refreshPinnedRepos(): void { public refreshPinnedRepos(): void {
const token = this.gitHubOAuthService.getTokenObservable()(); this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
if (token) {
this.junoClient.getPinnedRepos(token.scope);
}
} }
public async openPublishNotebookPane( public async openPublishNotebookPane(

View File

@@ -3,7 +3,6 @@ import "./base.css";
import "./default.css"; import "./default.css";
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components"; import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import { AzureTheme } from "./AzureTheme"; import { AzureTheme } from "./AzureTheme";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -16,7 +15,6 @@ import "./NotebookReadOnlyRenderer.less";
export interface NotebookRendererProps { export interface NotebookRendererProps {
contentRef: any; contentRef: any;
hideInputs?: boolean; hideInputs?: boolean;
hidePrompts?: boolean;
} }
interface PassedEditorProps { interface PassedEditorProps {
@@ -40,29 +38,6 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
loadTransform(this.props as any); loadTransform(this.props as any);
} }
private renderPrompt(id: string, contentRef: string): JSX.Element {
if (this.props.hidePrompts) {
return <></>;
}
return (
<Prompt id={id} contentRef={contentRef}>
{(props: PassedPromptProps) => {
if (props.status === "busy") {
return <React.Fragment>{"[*]"}</React.Fragment>;
}
if (props.status === "queued") {
return <React.Fragment>{"[…]"}</React.Fragment>;
}
if (typeof props.executionCount === "number") {
return <React.Fragment>{`[${props.executionCount}]`}</React.Fragment>;
}
return <React.Fragment>{"[ ]"}</React.Fragment>;
}}
</Prompt>
);
}
render(): JSX.Element { render(): JSX.Element {
return ( return (
<div className="NotebookReadOnlyRender"> <div className="NotebookReadOnlyRender">
@@ -71,7 +46,6 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => ( code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
<CodeCell id={id} contentRef={contentRef}> <CodeCell id={id} contentRef={contentRef}>
{{ {{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
editor: { editor: {
codemirror: (props: PassedEditorProps) => codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} /> this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />

View File

@@ -115,10 +115,10 @@
<!-- Database provisioned throughput - Start --> <!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput --> <!-- ko if: canConfigureThroughput -->
<div class="databaseProvision" aria-label="Provision database throughput" <div class="databaseProvision" aria-label="New database provision support"
data-bind="visible: databaseCreateNew"> data-bind="visible: databaseCreateNew">
<input tabindex="0" type="checkbox" data-test="addCollectionPane-databaseSharedThroughput" <input tabindex="0" type="checkbox" data-test="addCollectionPane-databaseSharedThroughput"
id="addCollection-databaseSharedThroughput" title="Provision database throughput" id="addCollection-databaseSharedThroughput" title="Provision shared throughput"
data-bind="checked: databaseCreateNewShared" /> data-bind="checked: databaseCreateNewShared" />
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span> <span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span>
<span class="infoTooltip" role="tooltip" tabindex="0"> <span class="infoTooltip" role="tooltip" tabindex="0">
@@ -517,13 +517,13 @@
<div> <div>
<span class="mandatoryStar">*</span> <span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Analytical store</span> <span class="addCollectionLabel">Analytical store</span>
<span class="infoTooltip" role="tooltip" tabindex="0" data-bind="event: { focus: function(data, event) { transferFocus('tooltip1', 'link1') } }"> <span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information"> <img class="infoImg" src="/info-bubble.svg" alt="More information">
<span id="tooltip1" class="tooltiptext infoTooltipWidth" data-bind="event: { mouseout: onMouseOut }"> <span class="tooltiptext infoTooltipWidth">
Enable analytical store capability to perform near real-time analytics on your operational Enable analytical store capability to perform near real-time analytics on your operational
data, without impacting the performance of transactional workloads. data, without impacting the performance of transactional workloads.
Learn more <a id="link1" class="errorLink" href="https://aka.ms/analytical-store-overview" Learn more <a class="errorLink" href="https://aka.ms/analytical-store-overview"
target="_blank" data-bind="event: { focusout: onFocusOut, keydown: onKeyDown.bind($data, 'largePartitionKey') }">here</a> target="_blank">here</a>
</span> </span>
</span> </span>
</div> </div>

View File

@@ -666,7 +666,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
const subscriptionType: ViewModels.SubscriptionType = const subscriptionType: ViewModels.SubscriptionType =
this.container.subscriptionType && this.container.subscriptionType(); this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) { if (subscriptionType === ViewModels.SubscriptionType.EA) {
return false; return false;
} }
@@ -681,7 +681,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true; return true;
}; };
public async open(databaseId?: string) { public open(databaseId?: string) {
super.open(); super.open();
// TODO: Figure out if a database level partition split is about to happen once shared throughput read is available // TODO: Figure out if a database level partition split is about to happen once shared throughput read is available
this.formWarnings(""); this.formWarnings("");
@@ -715,40 +715,18 @@ export default class AddCollectionPane extends ContextualPaneBase {
dataExplorerArea: Constants.Areas.ContextualPane dataExplorerArea: Constants.Areas.ContextualPane
}; };
await this.container.loadDatabaseOffers();
this._onDatabasesChange(this.container.databases()); this._onDatabasesChange(this.container.databases());
this._setFocus(); this._setFocus();
TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage); TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage);
} }
private transferFocus(elementIdToKeepVisible: string, elementIdToFocus: string): void {
document.getElementById(elementIdToKeepVisible).style.visibility = "visible";
document.getElementById(elementIdToFocus).focus();
}
private onFocusOut(_: any, event: any): void {
event.target.parentElement.style.visibility = "";
}
private onMouseOut(_: any, event: any): void {
event.target.style.visibility = "";
}
private onKeyDown(previousActiveElementId: string, _: any, event: KeyboardEvent): boolean {
if (event.shiftKey && event.keyCode == Constants.KeyCodes.Tab) {
document.getElementById(previousActiveElementId).focus();
return false;
} else {
// Execute default action
return true;
}
}
private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) { private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) {
const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => { const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => {
if (database && database.offer && database.offer()) { if (database && database.offer && database.offer()) {
this._databaseOffers.set(database.id(), database.offer()); this._databaseOffers.set(database.id(), database.offer());
} else if (database && database.isDatabaseShared && database.isDatabaseShared()) {
database.readSettings();
} }
return database.id(); return database.id();

View File

@@ -337,7 +337,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
const subscriptionType: ViewModels.SubscriptionType = const subscriptionType: ViewModels.SubscriptionType =
this.container.subscriptionType && this.container.subscriptionType(); this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) { if (subscriptionType === ViewModels.SubscriptionType.EA) {
return false; return false;
} }

View File

@@ -268,6 +268,8 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => { const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
if (keyspace && keyspace.offer && !!keyspace.offer()) { if (keyspace && keyspace.offer && !!keyspace.offer()) {
this.keyspaceOffers.set(keyspace.id(), keyspace.offer()); this.keyspaceOffers.set(keyspace.id(), keyspace.offer());
} else if (keyspace && keyspace.isDatabaseShared && keyspace.isDatabaseShared()) {
keyspace.readSettings();
} }
return keyspace.id(); return keyspace.id();
}); });

View File

@@ -9,7 +9,6 @@ import Explorer from "../Explorer";
// TODO: Use specific actions for logging telemetry data // TODO: Use specific actions for logging telemetry data
export abstract class ContextualPaneBase extends WaitsForTemplateViewModel { export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
private initalFocusedElement: HTMLElement | undefined;
public id: string; public id: string;
public container: Explorer; public container: Explorer;
public firstFieldHasFocus: ko.Observable<boolean>; public firstFieldHasFocus: ko.Observable<boolean>;
@@ -50,11 +49,9 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
this.visible(false); this.visible(false);
this.isExecuting(false); this.isExecuting(false);
this.resetData(); this.resetData();
this.resetFocus();
} }
public open() { public open() {
this.initalFocusedElement = document.activeElement as HTMLElement;
this.visible(true); this.visible(true);
this.firstFieldHasFocus(true); this.firstFieldHasFocus(true);
this.resizePane(); this.resizePane();
@@ -126,11 +123,4 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
$(paneElement).height(newPaneElementHeight); $(paneElement).height(newPaneElementHeight);
} }
private resetFocus(): void {
if (this.initalFocusedElement) {
this.initalFocusedElement.focus();
this.initalFocusedElement = undefined;
}
}
} }

View File

@@ -67,7 +67,8 @@
name="collectionIdConfirmation" name="collectionIdConfirmation"
required required
class="collid" class="collid"
data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus, attr: { 'aria-label': collectionIdConfirmationText }" data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus"
aria-label="Confirm by typing the collection id"
/> />
</p> </p>
</div> </div>

View File

@@ -134,9 +134,11 @@ describe("Delete Collection Confirmation Pane", () => {
expect(telemetryProcessorSpy.called).toBe(true); expect(telemetryProcessorSpy.called).toBe(true);
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback); let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
expect( expect(
telemetryProcessorSpy.calledWith(Action.DeleteCollection, ActionModifiers.Mark, { telemetryProcessorSpy.calledWith(
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)) Action.DeleteCollection,
}) ActionModifiers.Mark,
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
)
).toBe(true); ).toBe(true);
}); });
}); });

View File

@@ -88,9 +88,11 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
this.containerDeleteFeedback() this.containerDeleteFeedback()
); );
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, { TelemetryProcessor.trace(
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)) Action.DeleteCollection,
}); ActionModifiers.Mark,
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
);
this.containerDeleteFeedback(""); this.containerDeleteFeedback("");
} }

View File

@@ -120,9 +120,11 @@ describe("Delete Database Confirmation Pane", () => {
return pane.submit().then(() => { return pane.submit().then(() => {
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback); let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, { expect(TelemetryProcessor.trace).toHaveBeenCalledWith(
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)) Action.DeleteDatabase,
}); ActionModifiers.Mark,
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
);
}); });
}); });
}); });

View File

@@ -97,9 +97,11 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
this.databaseDeleteFeedback() this.databaseDeleteFeedback()
); );
TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, { TelemetryProcessor.trace(
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)) Action.DeleteDatabase,
}); ActionModifiers.Mark,
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
);
this.databaseDeleteFeedback(""); this.databaseDeleteFeedback("");
} }
@@ -130,8 +132,7 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
super.resetData(); super.resetData();
} }
public async open() { public open() {
await this.container.loadSelectedDatabaseOffer();
this.recordDeleteFeedback(this.shouldRecordFeedback()); this.recordDeleteFeedback(this.shouldRecordFeedback());
super.open(); super.open();
} }

View File

@@ -69,7 +69,6 @@ export default class AddTableEntityPane extends TableEntityPane {
); );
this.updateIsActionEnabled(); this.updateIsActionEnabled();
super.open(); super.open();
this.focusValueElement();
}); });
} else { } else {
this.displayedAttributes( this.displayedAttributes(
@@ -80,11 +79,7 @@ export default class AddTableEntityPane extends TableEntityPane {
); );
this.updateIsActionEnabled(); this.updateIsActionEnabled();
super.open(); super.open();
this.focusValueElement();
} }
}
private focusValueElement() {
const focusElement = document.getElementById("addTableEntityValue"); const focusElement = document.getElementById("addTableEntityValue");
focusElement && focusElement.focus(); focusElement && focusElement.focus();
} }

View File

@@ -21,7 +21,6 @@
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
id="closeAddEntityPane"
class="closeImg" class="closeImg"
role="button" role="button"
aria-label="Close pane" aria-label="Close pane"

View File

@@ -6,6 +6,7 @@ import { AuthType } from "../../AuthType";
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as Entities from "./Entities"; import * as Entities from "./Entities";
import EnvironmentUtility from "../../Common/EnvironmentUtility";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
@@ -307,7 +308,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestQueryApi ? Constants.CassandraBackend.guestQueryApi
: Constants.CassandraBackend.queryApi; : Constants.CassandraBackend.queryApi;
$.ajax(`${collection.container.extensionEndpoint()}${apiEndpoint}`, { $.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`, {
type: "POST", type: "POST",
data: { data: {
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name, accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
@@ -558,7 +559,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestKeysApi ? Constants.CassandraBackend.guestKeysApi
: Constants.CassandraBackend.keysApi; : Constants.CassandraBackend.keysApi;
let endpoint = `${collection.container.extensionEndpoint()}${apiEndpoint}`; let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`;
const deferred = Q.defer<CassandraTableKeys>(); const deferred = Q.defer<CassandraTableKeys>();
$.ajax(endpoint, { $.ajax(endpoint, {
type: "POST", type: "POST",
@@ -613,7 +614,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestSchemaApi ? Constants.CassandraBackend.guestSchemaApi
: Constants.CassandraBackend.schemaApi; : Constants.CassandraBackend.schemaApi;
let endpoint = `${collection.container.extensionEndpoint()}${apiEndpoint}`; let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`;
const deferred = Q.defer<CassandraTableKey[]>(); const deferred = Q.defer<CassandraTableKey[]>();
$.ajax(endpoint, { $.ajax(endpoint, {
type: "POST", type: "POST",
@@ -667,7 +668,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestCreateOrDeleteApi ? Constants.CassandraBackend.guestCreateOrDeleteApi
: Constants.CassandraBackend.createOrDeleteApi; : Constants.CassandraBackend.createOrDeleteApi;
$.ajax(`${explorer.extensionEndpoint()}${apiEndpoint}`, { $.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(explorer)}${apiEndpoint}`, {
type: "POST", type: "POST",
data: { data: {
accountName: explorer.databaseAccount() && explorer.databaseAccount().name, accountName: explorer.databaseAccount() && explorer.databaseAccount().name,

View File

@@ -598,6 +598,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
() => { () => {
this.container.isRefreshingExplorer(false); this.container.isRefreshingExplorer(false);
this._setBaseline(); this._setBaseline();
this.database.readSettings();
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.UpdateSettings, Action.UpdateSettings,
{ {
@@ -642,9 +643,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
}; };
public onActivate(): Q.Promise<any> { public onActivate(): Q.Promise<any> {
return super.onActivate().then(async () => { return super.onActivate().then(() => {
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
await this.database.loadOffer();
}); });
} }

View File

@@ -2,6 +2,7 @@ import * as Constants from "../../Common/Constants";
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import AuthHeadersUtil from "../../Platform/Hosted/Authorization"; import AuthHeadersUtil from "../../Platform/Hosted/Authorization";
import EnvironmentUtility from "../../Common/EnvironmentUtility";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import Q from "q"; import Q from "q";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
@@ -108,7 +109,11 @@ export default class MongoShellTab extends TabsBase {
) + Constants.MongoDBAccounts.defaultPort.toString(); ) + Constants.MongoDBAccounts.defaultPort.toString();
const databaseId = this.collection.databaseId; const databaseId = this.collection.databaseId;
const collectionId = this.collection.id(); const collectionId = this.collection.id();
const apiEndpoint = this._container.extensionEndpoint(); const apiEndpoint = EnvironmentUtility.getMongoBackendEndpoint(
this._container.serverId(),
userContext.databaseAccount.location,
this._container.extensionEndpoint()
).replace("/api/mongo/explorer", "");
const encryptedAuthToken: string = userContext.accessToken; const encryptedAuthToken: string = userContext.accessToken;
shellIframe.contentWindow.postMessage( shellIframe.contentWindow.postMessage(
@@ -137,7 +142,7 @@ export default class MongoShellTab extends TabsBase {
return; return;
} }
const dataToLog = { message: event.data.data.logData }; const dataToLog: string = event.data.data.logData;
const logType: string = event.data.data.logType; const logType: string = event.data.data.logType;
const shellTraceId: string = event.data.data.traceId || "none"; const shellTraceId: string = event.data.data.traceId || "none";

View File

@@ -346,6 +346,7 @@ describe("Settings tab", () => {
const offer: DataModels.Offer = null; const offer: DataModels.Offer = null;
const defaultTtl = 200; const defaultTtl = 200;
const database = new Database(explorer, baseDatabase, null);
const conflictResolutionPolicy = { const conflictResolutionPolicy = {
mode: DataModels.ConflictResolutionMode.LastWriterWins, mode: DataModels.ConflictResolutionMode.LastWriterWins,
conflictResolutionPath: "/_ts" conflictResolutionPath: "/_ts"
@@ -506,6 +507,7 @@ describe("Settings tab", () => {
} }
} }
}; };
const database = new Database(explorer, baseDatabase, null);
const container: DataModels.Collection = { const container: DataModels.Collection = {
_rid: "_rid", _rid: "_rid",
_self: "", _self: "",

View File

@@ -1270,10 +1270,8 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
} }
public onActivate(): Q.Promise<any> { public onActivate(): Q.Promise<any> {
return super.onActivate().then(async () => { return super.onActivate().then(() => {
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
const database: ViewModels.Database = this.collection.getDatabase();
await database.loadOffer();
}); });
} }

View File

@@ -12,8 +12,8 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { readOffers, readOffer } from "../../Common/DocumentClientUtilityBase";
import { readCollections } from "../../Common/dataAccess/readCollections"; import { readCollections } from "../../Common/dataAccess/readCollections";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
export default class Database implements ViewModels.Database { export default class Database implements ViewModels.Database {
public nodeKind: string; public nodeKind: string;
@@ -27,13 +27,13 @@ export default class Database implements ViewModels.Database {
public isDatabaseShared: ko.Computed<boolean>; public isDatabaseShared: ko.Computed<boolean>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>; public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
constructor(container: Explorer, data: any) { constructor(container: Explorer, data: any, offer: DataModels.Offer) {
this.nodeKind = "Database"; this.nodeKind = "Database";
this.container = container; this.container = container;
this.self = data._self; this.self = data._self;
this.rid = data._rid; this.rid = data._rid;
this.id = ko.observable(data.id); this.id = ko.observable(data.id);
this.offer = ko.observable(); this.offer = ko.observable(offer);
this.collections = ko.observableArray<Collection>(); this.collections = ko.observableArray<Collection>();
this.isDatabaseExpanded = ko.observable<boolean>(false); this.isDatabaseExpanded = ko.observable<boolean>(false);
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>(); this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
@@ -66,7 +66,7 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Scale" tabTitle: "Scale"
}); });
pendingNotificationsPromise.then( Q.all([pendingNotificationsPromise, this.readSettings()]).then(
(data: any) => { (data: any) => {
const pendingNotification: DataModels.Notification = data && data[0]; const pendingNotification: DataModels.Notification = data && data[0];
settingsTab = new DatabaseSettingsTab({ settingsTab = new DatabaseSettingsTab({
@@ -121,6 +121,80 @@ export default class Database implements ViewModels.Database {
} }
}; };
public readSettings(): Q.Promise<void> {
const deferred: Q.Deferred<void> = Q.defer<void>();
this.container.isRefreshingExplorer(true);
const databaseDataModel: DataModels.Database = <DataModels.Database>{
id: this.id(),
_rid: this.rid,
_self: this.self
};
const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience()
});
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers({
isServerless: this.container.isServerlessEnabled()
});
Q.all([offerInfoPromise]).then(
() => {
this.container.isRefreshingExplorer(false);
const databaseOffer: DataModels.Offer = this._getOfferForDatabase(
offerInfoPromise.valueOf(),
databaseDataModel
);
if (!databaseOffer) {
return;
}
readOffer(databaseOffer).then((offerDetail: DataModels.OfferWithHeaders) => {
const offerThroughputInfo: DataModels.OfferThroughputInfo = {
minimumRUForCollection:
offerDetail.content &&
offerDetail.content.collectionThroughputInfo &&
offerDetail.content.collectionThroughputInfo.minimumRUForCollection,
numPhysicalPartitions:
offerDetail.content &&
offerDetail.content.collectionThroughputInfo &&
offerDetail.content.collectionThroughputInfo.numPhysicalPartitions
};
databaseOffer.content.collectionThroughputInfo = offerThroughputInfo;
(databaseOffer as DataModels.OfferWithHeaders).headers = offerDetail.headers;
this.offer(databaseOffer);
this.offer.valueHasMutated();
TelemetryProcessor.traceSuccess(
Action.LoadOffers,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience()
},
startKey
);
deferred.resolve();
});
},
(error: any) => {
this.container.isRefreshingExplorer(false);
deferred.reject(error);
TelemetryProcessor.traceFailure(
Action.LoadOffers,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience()
},
startKey
);
}
);
return deferred.promise;
}
public isDatabaseNodeSelected(): boolean { public isDatabaseNodeSelected(): boolean {
return ( return (
!this.isDatabaseExpanded() && !this.isDatabaseExpanded() &&
@@ -145,13 +219,23 @@ export default class Database implements ViewModels.Database {
}); });
} }
public async expandDatabase() { public expandCollapseDatabase() {
this.selectDatabase();
if (this.isDatabaseExpanded()) {
this.collapseDatabase();
} else {
this.expandDatabase();
}
this.container.onUpdateTabsButtons([]);
this.container.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === this.rid);
}
public expandDatabase() {
if (this.isDatabaseExpanded()) { if (this.isDatabaseExpanded()) {
return; return;
} }
await this.loadOffer(); this.loadCollections();
await this.loadCollections();
this.isDatabaseExpanded(true); this.isDatabaseExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Database node", description: "Database node",
@@ -175,19 +259,32 @@ export default class Database implements ViewModels.Database {
}); });
} }
public async loadCollections(): Promise<void> { public loadCollections(): Q.Promise<void> {
const collectionVMs: Collection[] = []; let collectionVMs: Collection[] = [];
const collections: DataModels.Collection[] = await readCollections(this.id()); let deferred: Q.Deferred<void> = Q.defer<void>();
const deltaCollections = this.getDeltaCollections(collections);
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => { readCollections(this.id()).then(
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null); (collections: DataModels.Collection[]) => {
collectionVMs.push(collectionVM); let collectionsToAddVMPromises: Q.Promise<any>[] = [];
}); let deltaCollections = this.getDeltaCollections(collections);
//merge collections deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
this.addCollectionsToList(collectionVMs); const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
this.deleteCollectionsFromList(deltaCollections.toDelete); collectionVMs.push(collectionVM);
});
//merge collections
this.addCollectionsToList(collectionVMs);
this.deleteCollectionsFromList(deltaCollections.toDelete);
deferred.resolve();
},
(error: any) => {
deferred.reject(error);
}
);
return deferred.promise;
} }
public openAddCollection(database: Database, event: MouseEvent) { public openAddCollection(database: Database, event: MouseEvent) {
@@ -199,16 +296,6 @@ export default class Database implements ViewModels.Database {
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId); return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId);
} }
public async loadOffer(): Promise<void> {
if (!this.container.isServerlessEnabled() && !this.offer()) {
const params: DataModels.ReadDatabaseOfferParams = {
databaseId: this.id(),
databaseResourceId: this.self
};
this.offer(await readDatabaseOffer(params));
}
}
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> { private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
if (!this.container) { if (!this.container) {
return Q.resolve(undefined); return Q.resolve(undefined);
@@ -289,10 +376,6 @@ export default class Database implements ViewModels.Database {
} }
private deleteCollectionsFromList(collectionsToRemove: Collection[]): void { private deleteCollectionsFromList(collectionsToRemove: Collection[]): void {
if (collectionsToRemove.length === 0) {
return;
}
const collectionsToKeep: Collection[] = []; const collectionsToKeep: Collection[] = [];
ko.utils.arrayForEach(this.collections(), (collection: Collection) => { ko.utils.arrayForEach(this.collections(), (collection: Collection) => {
@@ -304,4 +387,8 @@ export default class Database implements ViewModels.Database {
this.collections(collectionsToKeep); this.collections(collectionsToKeep);
} }
private _getOfferForDatabase(offers: DataModels.Offer[], database: DataModels.Database): DataModels.Offer {
return _.find(offers, (offer: DataModels.Offer) => offer.resource === database._self);
}
} }

View File

@@ -64,7 +64,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.container.nonSystemDatabases.subscribe((databases: ViewModels.Database[]) => { this.container.nonSystemDatabases.subscribe((databases: ViewModels.Database[]) => {
// Clean up old databases // Clean up old databases
this.cleanupDatabasesKoSubs(); this.cleanupDatabasesKoSubs(databases.map((database: ViewModels.Database) => database.id()));
databases.forEach((database: ViewModels.Database) => this.watchDatabase(database)); databases.forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.triggerRender(); this.triggerRender();
@@ -170,17 +170,14 @@ export class ResourceTreeAdapter implements ReactAdapter {
children: [], children: [],
isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined), isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database), contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database),
onClick: async isExpanded => { onClick: isExpanded => {
// Rewritten version of expandCollapseDatabase(): // Rewritten version of expandCollapseDatabase():
if (isExpanded) { if (!isExpanded) {
database.collapseDatabase(); database.expandDatabase();
database.loadCollections();
} else { } else {
if (databaseNode.children?.length === 0) { database.collapseDatabase();
databaseNode.isLoading = true;
}
await database.expandDatabase();
} }
databaseNode.isLoading = false;
database.selectDatabase(); database.selectDatabase();
this.container.onUpdateTabsButtons([]); this.container.onUpdateTabsButtons([]);
this.container.tabsManager.refreshActiveTab( this.container.tabsManager.refreshActiveTab(
@@ -206,12 +203,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
databaseNode.children.push(this.buildCollectionNode(database, collection)) databaseNode.children.push(this.buildCollectionNode(database, collection))
); );
database.collections.subscribe((collections: ViewModels.Collection[]) => {
collections.forEach((collection: ViewModels.Collection) =>
databaseNode.children.push(this.buildCollectionNode(database, collection))
);
});
return databaseNode; return databaseNode;
}); });
@@ -799,10 +790,16 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.koSubsCollectionIdMap.push(collectionId, sub); this.koSubsCollectionIdMap.push(collectionId, sub);
} }
private cleanupDatabasesKoSubs(): void { private cleanupDatabasesKoSubs(existingDatabaseIds: string[]): void {
this.koSubsDatabaseIdMap.keys().forEach((databaseId: string) => { const databaseIdsToRemove = this.databaseCollectionIdMap
this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose()); .keys()
this.koSubsDatabaseIdMap.delete(databaseId); .filter((id: string) => existingDatabaseIds.indexOf(id) === -1);
databaseIdsToRemove.forEach((databaseId: string) => {
if (this.koSubsDatabaseIdMap.has(databaseId)) {
this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose());
this.koSubsDatabaseIdMap.delete(databaseId);
}
if (this.databaseCollectionIdMap.has(databaseId)) { if (this.databaseCollectionIdMap.has(databaseId)) {
this.databaseCollectionIdMap this.databaseCollectionIdMap

View File

@@ -35,19 +35,13 @@ const onInit = async () => {
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId); const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
galleryItem = galleryItemJunoResponse.data; galleryItem = galleryItemJunoResponse.data;
} }
render(notebookUrl, backNavigationText, hideInputs, galleryItem, onBackClick);
// The main purpose of hiding the prompt is to hide everything when hiding inputs.
// It is generally not very useful to just hide the prompt.
const hidePrompts = hideInputs;
render(notebookUrl, backNavigationText, hideInputs, hidePrompts, galleryItem, onBackClick);
}; };
const render = ( const render = (
notebookUrl: string, notebookUrl: string,
backNavigationText: string, backNavigationText: string,
hideInputs?: boolean, hideInputs: boolean,
hidePrompts?: boolean,
galleryItem?: IGalleryItem, galleryItem?: IGalleryItem,
onBackClick?: () => void onBackClick?: () => void
) => { ) => {
@@ -57,7 +51,6 @@ const render = (
galleryItem, galleryItem,
backNavigationText, backNavigationText,
hideInputs, hideInputs,
hidePrompts,
onBackClick: onBackClick, onBackClick: onBackClick,
onTagClick: undefined onTagClick: undefined
}; };

View File

@@ -4,15 +4,12 @@ import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { appInsights } from "../appInsights"; import { appInsights } from "../appInsights";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getDataExplorerWindow } from "../../Utils/WindowUtils";
/** /**
* Class that persists telemetry data to the portal tables. * Class that persists telemetry data to the portal tables.
*/ */
type TelemetryData = { [key: string]: unknown }; export function trace(action: Action, actionModifier: string = ActionModifiers.Mark, data?: unknown): void {
export function trace(action: Action, actionModifier: string = ActionModifiers.Mark, data?: TelemetryData): void {
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
data: { data: {
@@ -25,7 +22,7 @@ export function trace(action: Action, actionModifier: string = ActionModifiers.M
appInsights.trackEvent({ name: Action[action] }, getData(actionModifier, data)); appInsights.trackEvent({ name: Action[action] }, getData(actionModifier, data));
} }
export function traceStart(action: Action, data?: TelemetryData): number { export function traceStart(action: Action, data?: unknown): number {
const timestamp: number = Date.now(); const timestamp: number = Date.now();
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
@@ -41,7 +38,7 @@ export function traceStart(action: Action, data?: TelemetryData): number {
return timestamp; return timestamp;
} }
export function traceSuccess(action: Action, data?: TelemetryData, timestamp?: number): void { export function traceSuccess(action: Action, data?: unknown, timestamp?: number): void {
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
data: { data: {
@@ -55,7 +52,7 @@ export function traceSuccess(action: Action, data?: TelemetryData, timestamp?: n
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Success, data)); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Success, data));
} }
export function traceFailure(action: Action, data?: TelemetryData, timestamp?: number): void { export function traceFailure(action: Action, data?: unknown, timestamp?: number): void {
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
data: { data: {
@@ -69,7 +66,7 @@ export function traceFailure(action: Action, data?: TelemetryData, timestamp?: n
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Failed, data)); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Failed, data));
} }
export function traceCancel(action: Action, data?: TelemetryData, timestamp?: number): void { export function traceCancel(action: Action, data?: unknown, timestamp?: number): void {
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
data: { data: {
@@ -83,7 +80,7 @@ export function traceCancel(action: Action, data?: TelemetryData, timestamp?: nu
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Cancel, data)); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Cancel, data));
} }
export function traceOpen(action: Action, data?: TelemetryData, timestamp?: number): number { export function traceOpen(action: Action, data?: unknown, timestamp?: number): number {
const validTimestamp = timestamp || Date.now(); const validTimestamp = timestamp || Date.now();
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
@@ -99,7 +96,7 @@ export function traceOpen(action: Action, data?: TelemetryData, timestamp?: numb
return validTimestamp; return validTimestamp;
} }
export function traceMark(action: Action, data?: TelemetryData, timestamp?: number): number { export function traceMark(action: Action, data?: unknown, timestamp?: number): number {
const validTimestamp = timestamp || Date.now(); const validTimestamp = timestamp || Date.now();
sendMessage({ sendMessage({
type: MessageTypes.TelemetryInfo, type: MessageTypes.TelemetryInfo,
@@ -115,16 +112,21 @@ export function traceMark(action: Action, data?: TelemetryData, timestamp?: numb
return validTimestamp; return validTimestamp;
} }
function getData(actionModifier: string, data: TelemetryData = {}): { [key: string]: string } { function getData(actionModifier: string, data: unknown = {}): { [key: string]: string } | undefined {
const dataExplorerWindow = getDataExplorerWindow(window); if (typeof data === "string") {
return { data = { message: data };
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet }
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (typeof data === "object") {
authType: dataExplorerWindow && (dataExplorerWindow as any).authType, return {
subscriptionId: userContext.subscriptionId as string, // TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
platform: configContext.platform, // eslint-disable-next-line @typescript-eslint/no-explicit-any
env: process.env.NODE_ENV as string, authType: (window as any).authType,
actionModifier, subscriptionId: userContext.subscriptionId as string,
...data platform: configContext.platform,
}; env: process.env.NODE_ENV as string,
actionModifier,
...data
};
}
return undefined;
} }

View File

@@ -6,8 +6,6 @@ import { ServerConnection } from "@jupyterlab/services";
import { JupyterLabAppFactory } from "./JupyterLabAppFactory"; import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
import { Action } from "../Shared/Telemetry/TelemetryConstants"; import { Action } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../UserContext";
import { TerminalQueryParams } from "../Common/Constants";
const getUrlVars = (): { [key: string]: string } => { const getUrlVars = (): { [key: string]: string } => {
const vars: { [key: string]: string } = {}; const vars: { [key: string]: string } = {};
@@ -20,22 +18,22 @@ const getUrlVars = (): { [key: string]: string } => {
const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => { const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => {
let body: BodyInit; let body: BodyInit;
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) { if (urlVars.hasOwnProperty("terminalEndpoint")) {
body = JSON.stringify({ body = JSON.stringify({
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint] endpoint: urlVars["terminalEndpoint"]
}); });
} }
const server = urlVars[TerminalQueryParams.Server]; const server = urlVars["server"];
let options: Partial<ServerConnection.ISettings> = { let options: Partial<ServerConnection.ISettings> = {
baseUrl: server, baseUrl: server,
init: { body }, init: { body },
fetch: window.parent.fetch fetch: window.parent.fetch
}; };
if (urlVars.hasOwnProperty(TerminalQueryParams.Token)) { if (urlVars.hasOwnProperty("token")) {
options = { options = {
baseUrl: server, baseUrl: server,
token: urlVars[TerminalQueryParams.Token], token: urlVars["token"],
init: { body }, init: { body },
fetch: window.parent.fetch fetch: window.parent.fetch
}; };
@@ -46,12 +44,6 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
const urlVars = getUrlVars(); const urlVars = getUrlVars();
// Initialize userContext. Currently only subscrptionId is required by TelemetryProcessor
updateUserContext({
subscriptionId: urlVars[TerminalQueryParams.SubscriptionId]
});
const serverSettings = createServerSettings(urlVars); const serverSettings = createServerSettings(urlVars);
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, { const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, {
@@ -59,7 +51,7 @@ const main = async (): Promise<void> => {
}); });
try { try {
if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) { if (urlVars.hasOwnProperty("terminal")) {
await JupyterLabAppFactory.createTerminalApp(serverSettings); await JupyterLabAppFactory.createTerminalApp(serverSettings);
} else { } else {
throw new Error("Only terminal is supported"); throw new Error("Only terminal is supported");

View File

@@ -1,21 +0,0 @@
import { isInvalidParentFrameOrigin } from "./MessageValidation";
test.each`
domain | expected
${"https://cosmos.azure.com"} | ${false}
${"https://cosmos.azure.us"} | ${false}
${"https://cosmos.azure.cn"} | ${false}
${"https://cosmos.microsoftazure.de"} | ${false}
${"https://subdomain.portal.azure.com"} | ${false}
${"https://subdomain.portal.azure.us"} | ${false}
${"https://subdomain.portal.azure.cn"} | ${false}
${"https://subdomain.microsoftazure.de"} | ${false}
${"https://main.documentdb.ext.azure.com"} | ${false}
${"https://main.documentdb.ext.azure.us"} | ${false}
${"https://main.documentdb.ext.azure.cn"} | ${false}
${"https://main.documentdb.ext.microsoftazure.de"} | ${false}
${"https://random.domain"} | ${true}
${"https://malicious.cloudapp.azure.com"} | ${true}
`("returns $expected when called with $domain", ({ domain, expected }) => {
expect(isInvalidParentFrameOrigin({ origin: domain } as MessageEvent)).toBe(expected);
});

View File

@@ -4,18 +4,13 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
return !isValidOrigin(configContext.allowedParentFrameOrigins, event); return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
} }
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean { function isValidOrigin(allowedOrigins: RegExp, event: MessageEvent): boolean {
const eventOrigin = (event && event.origin) || ""; const eventOrigin = (event && event.origin) || "";
const windowOrigin = (window && window.origin) || ""; const windowOrigin = (window && window.origin) || "";
if (eventOrigin === windowOrigin) { if (eventOrigin === windowOrigin) {
return true; return true;
} }
for (const origin of allowedOrigins) { const result = allowedOrigins && allowedOrigins.test(eventOrigin);
const result = new RegExp(origin).test(eventOrigin); return result;
if (result) {
return true;
}
}
return false;
} }

View File

@@ -1,49 +0,0 @@
import { getDataExplorerWindow } from "./WindowUtils";
const createWindow = (dataExplorerPlatform: unknown, parent: Window): Window => {
// TODO: Need to `any` here since we're creating a mock window object
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockWindow: any = {};
if (dataExplorerPlatform !== undefined) {
mockWindow.dataExplorerPlatform = dataExplorerPlatform;
}
if (parent) {
mockWindow.parent = parent;
}
return mockWindow;
};
describe("WindowUtils", () => {
describe("getDataExplorerWindow", () => {
it("should return current window if current window has dataExplorerPlatform property", () => {
const currentWindow = createWindow(0, undefined);
expect(getDataExplorerWindow(currentWindow)).toEqual(currentWindow);
});
it("should return current window's parent if current window's parent has dataExplorerPlatform property", () => {
const parentWindow = createWindow(0, undefined);
const currentWindow = createWindow(undefined, parentWindow);
expect(getDataExplorerWindow(currentWindow)).toEqual(parentWindow);
});
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is reference to itself", () => {
const parentWindow = createWindow(undefined, undefined);
// TODO: Need to `any` here since parent is a readonly property
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(parentWindow as any).parent = parentWindow; // If a window does not have a parent, its parent property is a reference to itself.
const currentWindow = createWindow(undefined, parentWindow);
expect(getDataExplorerWindow(currentWindow)).toBeUndefined();
});
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is not defined", () => {
const parentWindow = createWindow(undefined, undefined);
const currentWindow = createWindow(undefined, parentWindow);
expect(getDataExplorerWindow(currentWindow)).toBeUndefined();
});
});
});

View File

@@ -1,23 +0,0 @@
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
// Start with the current window and traverse up the parent hierarchy to find a window
// with `dataExplorerPlatform` property
let dataExplorerWindow: Window | undefined = currentWindow;
try {
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
// eslint-disable-next-line @typescript-eslint/no-explicit-any
while (dataExplorerWindow && (dataExplorerWindow as any).dataExplorerPlatform === undefined) {
// If a window does not have a parent, its parent property is a reference to itself.
if (dataExplorerWindow.parent === dataExplorerWindow) {
dataExplorerWindow = undefined;
} else {
dataExplorerWindow = dataExplorerWindow.parent;
}
}
} catch (error) {
// This can happen if we come across parent from a different origin
dataExplorerWindow = undefined;
}
return dataExplorerWindow;
};

View File

@@ -1,98 +0,0 @@
import "expect-puppeteer";
import crypto from 'crypto'
jest.setTimeout(300000);
const RENDER_DELAY = 400
const LOADING_STATE_DELAY = 1800
describe('Collection Add and Delete Cassandra spec', () => {
it('creates a collection', async () => {
try {
const keyspaceId = `keyspaceid${crypto.randomBytes(8).toString("hex")}`;
const tableId = `tableid${crypto.randomBytes(3).toString('hex')}`;
const prodUrl = "https://localhost:1234/hostedExplorer.html";
page.goto(prodUrl);
// log in with connection string
const handle = await page.waitForSelector('iframe');
const frame = await handle.contentFrame();
await frame.waitFor('div > p.switchConnectTypeText', { visible: true });
await frame.click('div > p.switchConnectTypeText');
const connStr = process.env.CASSANDRA_CONNECTION_STRING;
await frame.type("input[class='inputToken']", connStr);
await frame.click("input[value='Connect']");
// create new table
await frame.waitFor('button[data-test="New Table"]', { visible: true });
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.click('button[data-test="New Table"]');
// type keyspace id
await frame.waitFor('input[id="keyspace-id"]', { visible: true });
await frame.type('input[id="keyspace-id"]', keyspaceId);
// type table id
await frame.waitFor('input[class="textfontclr"]');
await frame.type('input[class="textfontclr"]', tableId);
// click submit
await frame.waitFor('#cassandraaddcollectionpane > div > form > div.paneFooter > div > input');
await frame.click('#cassandraaddcollectionpane > div > form > div.paneFooter > div > input');
// open database menu
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(`div[data-test="${keyspaceId}"]`, { visible: true });
await frame.waitFor(LOADING_STATE_DELAY)
await frame.waitFor(`div[data-test="${keyspaceId}"]`, { visible: true });
await frame.click(`div[data-test="${keyspaceId}"]`);
await frame.waitFor(`span[title="${tableId}"]`, { visible: true });
// delete container
// click context menu for container
await frame.waitFor(`div[data-test="${tableId}"] > div > button`, { visible: true });
await frame.click(`div[data-test="${tableId}"] > div > button`);
// click delete container
await frame.waitForSelector('body > div.ms-Layer.ms-Layer--fixed');
await frame.waitFor(RENDER_DELAY)
const elements = await frame.$$('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]')
await elements[0].click()
// confirm delete container
await frame.type('input[data-test="confirmCollectionId"]', tableId.trim());
// click delete
await frame.click('input[data-test="deleteCollection"]');
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await expect(page).not.toMatchElement(`div[data-test="${tableId}"]`);
// click context menu for database
await frame.waitFor(`div[data-test="${keyspaceId}"] > div > button`);
const button = await frame.$(`div[data-test="${keyspaceId}"] > div > button`);
await button.focus();
await button.asElement().click();
// click delete database
await frame.waitFor(RENDER_DELAY);
const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]')
await dbElements[0].click();
// confirm delete database
await frame.type('input[data-test="confirmDatabaseId"]', keyspaceId.trim());
// click delete
await frame.click('input[data-test="deleteDatabase"]');
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await expect(page).not.toMatchElement(`div[data-test="${keyspaceId}"]`);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName
await page.screenshot({path: `Test Failed ${testName}.png`});
throw error;
}
});
});

View File

@@ -1,10 +1,10 @@
import "expect-puppeteer"; import "expect-puppeteer";
import crypto from "crypto"; import crypto from 'crypto'
jest.setTimeout(300000); jest.setTimeout(300000);
describe("Collection Add and Delete SQL spec", () => { describe('Collection Add and Delete SQL spec', () => {
it("creates a collection", async () => { it('creates a collection', async () => {
try { try {
const dbId = `TestDatabase${crypto.randomBytes(8).toString("hex")}`; const dbId = `TestDatabase${crypto.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypto.randomBytes(8).toString("hex")}`; const collectionId = `TestCollection${crypto.randomBytes(8).toString("hex")}`;
@@ -13,10 +13,10 @@ describe("Collection Add and Delete SQL spec", () => {
page.goto(prodUrl); page.goto(prodUrl);
// log in with connection string // log in with connection string
const handle = await page.waitForSelector("iframe"); const handle = await page.waitForSelector('iframe');
const frame = await handle.contentFrame(); const frame = await handle.contentFrame();
await frame.waitFor("div > p.switchConnectTypeText", { visible: true }); await frame.waitFor('div > p.switchConnectTypeText', { visible: true });
await frame.click("div > p.switchConnectTypeText"); await frame.click('div > p.switchConnectTypeText');
const connStr = process.env.PORTAL_RUNNER_CONNECTION_STRING; const connStr = process.env.PORTAL_RUNNER_CONNECTION_STRING;
await frame.type("input[class='inputToken']", connStr); await frame.type("input[class='inputToken']", connStr);
await frame.click("input[value='Connect']"); await frame.click("input[value='Connect']");
@@ -25,30 +25,30 @@ describe("Collection Add and Delete SQL spec", () => {
await frame.waitFor('button[data-test="New Container"]', { visible: true }); await frame.waitFor('button[data-test="New Container"]', { visible: true });
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.click('button[data-test="New Container"]'); await frame.click('button[data-test="New Container"]');
// check new database // check new database
await frame.waitFor('input[data-test="addCollection-createNewDatabase"]'); await frame.waitFor('input[data-test="addCollection-createNewDatabase"]');
await frame.click('input[data-test="addCollection-createNewDatabase"]'); await frame.click('input[data-test="addCollection-createNewDatabase"]');
// check shared throughput // check shared throughput
await frame.waitFor('input[data-test="addCollectionPane-databaseSharedThroughput"]'); await frame.waitFor('input[data-test="addCollectionPane-databaseSharedThroughput"]');
await frame.click('input[data-test="addCollectionPane-databaseSharedThroughput"]'); await frame.click('input[data-test="addCollectionPane-databaseSharedThroughput"]') ;
// type database id // type database id
await frame.waitFor('input[data-test="addCollection-newDatabaseId"]'); await frame.waitFor('input[data-test="addCollection-newDatabaseId"]');
await frame.type('input[data-test="addCollection-newDatabaseId"]', dbId); await frame.type('input[data-test="addCollection-newDatabaseId"]', dbId);
// type collection id // type collection id
await frame.waitFor('input[data-test="addCollection-collectionId"]'); await frame.waitFor('input[data-test="addCollection-collectionId"]');
await frame.type('input[data-test="addCollection-collectionId"]', collectionId); await frame.type('input[data-test="addCollection-collectionId"]', collectionId);
// type partition key value // type partition key value
await frame.waitFor('input[data-test="addCollection-partitionKeyValue"]'); await frame.waitFor('input[data-test="addCollection-partitionKeyValue"]');
await frame.type('input[data-test="addCollection-partitionKeyValue"]', sharedKey); await frame.type('input[data-test="addCollection-partitionKeyValue"]', sharedKey);
// click submit // click submit
await frame.waitFor("#submitBtnAddCollection"); await frame.waitFor('#submitBtnAddCollection');
await frame.click("#submitBtnAddCollection"); await frame.click('#submitBtnAddCollection');
// validate created // validate created
// open database menu // open database menu
@@ -56,27 +56,24 @@ describe("Collection Add and Delete SQL spec", () => {
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.click(`div[data-test="${dbId}"]`); await frame.click(`div[data-test="${dbId}"]`);
await frame.waitFor(3000); await frame.waitFor(`span[title="${collectionId}"]`);
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
// delete container // delete container
// click context menu for container // click context menu for container
await frame.waitFor(`div[data-test="${collectionId}"] > div > button`, { visible: true }); await frame.waitFor(`div[data-test="${collectionId}"] > div > button`);
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
await frame.click(`div[data-test="${collectionId}"] > div > button`); await frame.click(`div[data-test="${collectionId}"] > div > button`);
await frame.waitFor(2000);
// click delete container // click delete container
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]', { visible: true }); await frame.waitForSelector('body > div.ms-Layer.ms-Layer--fixed');
await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); await frame.waitFor(1000);
const elements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
await elements[4].click();
// confirm delete container // confirm delete container
await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true });
await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim()); await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim());
// click delete // click delete
await frame.waitFor('input[data-test="deleteCollection"]', { visible: true });
await frame.click('input[data-test="deleteCollection"]'); await frame.click('input[data-test="deleteCollection"]');
await frame.waitFor(5000); await frame.waitFor(5000);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
@@ -90,8 +87,9 @@ describe("Collection Add and Delete SQL spec", () => {
await button.asElement().click(); await button.asElement().click();
// click delete database // click delete database
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]'); await frame.waitFor(1000);
await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]'); const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
await dbElements[1].click();
// confirm delete database // confirm delete database
await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim()); await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim());
@@ -101,10 +99,8 @@ describe("Collection Add and Delete SQL spec", () => {
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await expect(page).not.toMatchElement(`div[data-test="${dbId}"]`); await expect(page).not.toMatchElement(`div[data-test="${dbId}"]`);
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any await page.screenshot({path: 'failure.png'});
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `Test Failed ${testName}.jpg` });
throw error; throw error;
} }
}); })
}); })

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"sourceMap": true, "sourceMap": false,
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,

View File

@@ -11,7 +11,6 @@ const childProcess = require("child_process");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
const isCI = require("is-ci"); const isCI = require("is-ci");
const webpack = require("webpack");
const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8"); const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8");
@@ -105,15 +104,6 @@ module.exports = function(env = {}, argv = {}) {
envVars.NODE_ENV = "development"; envVars.NODE_ENV = "development";
} }
const sourceMapPlugin =
mode === "development"
? new webpack.EvalSourceMapDevToolPlugin({})
: new webpack.SourceMapDevToolPlugin({
// test: [".js", ".mjs", ".css", ".ts", ".tsx"],
filename: "[name].js.map",
exclude: [/vendor/]
});
const plugins = [ const plugins = [
new CleanWebpackPlugin(["dist"]), new CleanWebpackPlugin(["dist"]),
new CreateFileWebpack({ new CreateFileWebpack({
@@ -174,9 +164,7 @@ module.exports = function(env = {}, argv = {}) {
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [{ from: "DataExplorer.nuspec" }, { from: "web.config" }, { from: "quickstart/*.zip" }] patterns: [{ from: "DataExplorer.nuspec" }, { from: "web.config" }, { from: "quickstart/*.zip" }]
}), }),
new EnvironmentPlugin(envVars), new EnvironmentPlugin(envVars)
new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
sourceMapPlugin
]; ];
if (argv.analyze) { if (argv.analyze) {
@@ -206,6 +194,7 @@ module.exports = function(env = {}, argv = {}) {
filename: "[name].[chunkhash:6].js", filename: "[name].[chunkhash:6].js",
path: path.resolve(__dirname, "dist") path: path.resolve(__dirname, "dist")
}, },
devtool: mode === "development" ? "cheap-eval-source-map" : "source-map",
plugins, plugins,
module: { module: {
rules rules
@@ -217,7 +206,6 @@ module.exports = function(env = {}, argv = {}) {
minimize: mode === "production" ? true : false, minimize: mode === "production" ? true : false,
minimizer: [ minimizer: [
new TerserPlugin({ new TerserPlugin({
sourceMap: true,
cache: ".cache/terser", cache: ".cache/terser",
terserOptions: { terserOptions: {
// These options increase our initial bundle size by ~5% but the builds are significantly faster and won't run out of memory // These options increase our initial bundle size by ~5% but the builds are significantly faster and won't run out of memory