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,
slowMo: 50,
defaultViewport: null,
ignoreHTTPSErrors: true,
args: ["--disable-web-security"]
ignoreHTTPSErrors: true
}
};

View File

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

View File

@@ -170,8 +170,89 @@ export enum MongoBackendEndpointType {
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
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 guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
public static readonly queryApi: string = "api/cassandra";
@@ -481,11 +562,3 @@ export class AnalyticalStorageTtl {
public static readonly Infinite: number = -1;
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 {
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 {
if (uri && uri.slice(-1) !== "/") {
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();
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 * as _ from "underscore";
import * as Constants from "./Constants";
import { getDataExplorerWindow } from "../Utils/WindowUtils";
export interface CachedDataPromise<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 {
return window.parent !== window;
}

View File

@@ -1,8 +1,9 @@
import { AuthType } from "../AuthType";
import { resetConfigContext, updateConfigContext } from "../ConfigContext";
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels";
import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
import { updateUserContext } from "../UserContext";
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
@@ -236,19 +237,19 @@ describe("MongoProxyClient", () => {
});
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");
});
it("returns a development endpoint", () => {
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");
});
it("returns a guest endpoint", () => {
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");
});
});

View File

@@ -10,6 +10,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { userContext } from "../UserContext";
import EnvironmentUtility from "./EnvironmentUtility";
import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler";
@@ -77,7 +78,7 @@ export function queryDocuments(
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
};
const endpoint = getEndpoint() || "";
const endpoint = getEndpoint(databaseAccount) || "";
const headers = {
...defaultHeaders,
@@ -138,7 +139,7 @@ export function readDocument(
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
};
const endpoint = getEndpoint();
const endpoint = getEndpoint(databaseAccount);
return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
method: "GET",
@@ -178,7 +179,7 @@ export function createDocument(
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
};
const endpoint = getEndpoint();
const endpoint = getEndpoint(databaseAccount);
return window
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
@@ -220,7 +221,7 @@ export function updateDocument(
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
};
const endpoint = getEndpoint();
const endpoint = getEndpoint(databaseAccount);
return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
@@ -259,7 +260,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
};
const endpoint = getEndpoint();
const endpoint = getEndpoint(databaseAccount);
return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
@@ -302,7 +303,7 @@ export function createMongoCollectionWithProxy(
autoPilotThroughput: params.autoPilotMaxThroughput?.toString()
};
const endpoint = getEndpoint();
const endpoint = getEndpoint(databaseAccount);
return window
.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();
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) {
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 {
platform: Platform;
allowedParentFrameOrigins: string[];
allowedParentFrameOrigins: RegExp;
gitSha?: string;
proxyPath?: string;
AAD_ENDPOINT: string;
@@ -30,12 +30,7 @@ interface ConfigContext {
// Default configuration
let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal,
allowedParentFrameOrigins: [
`^https:\\/\\/cosmos.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+.portal.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+.ext.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+microsoftazure.de$`
],
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$/,
// Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
@@ -78,13 +73,8 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
const response = await fetch("./config.json");
if (response.status === 200) {
try {
const { allowedParentFrameOrigins, ...externalConfig } = await response.json();
const externalConfig = await response.json();
Object.assign(configContext, externalConfig);
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
updateConfigContext({
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins]
});
}
} catch (error) {
console.error("Unable to parse json in config file");
console.error(error);

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,8 +8,6 @@ import * as Logger from "../../../Common/Logger";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { StringUtils } from "../../../Utils/StringUtils";
import { userContext } from "../../../UserContext";
import { TerminalQueryParams } from "../../../Common/Constants";
export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
@@ -34,11 +32,11 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
public getTerminalParams(): 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();
if (terminalEndpoint) {
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
params.set("terminalEndpoint", terminalEndpoint);
}
return params;
@@ -77,13 +75,11 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
return "";
}
params.set(TerminalQueryParams.Server, serverInfo.notebookServerEndpoint);
params.set("server", serverInfo.notebookServerEndpoint);
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?";
for (let key of params.keys()) {
result += `${key}=${encodeURIComponent(params.get(key))}&`;

View File

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

View File

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

View File

@@ -63,15 +63,6 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
label
</span>
</div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={
@@ -188,7 +179,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
"isBeakVisible": false,
"items": Array [
Object {
"className": undefined,
"disabled": true,
"key": "menuLabel",
"onClick": undefined,
@@ -211,15 +201,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
/>
</div>
</div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={
@@ -280,77 +261,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
</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`] = `
<div
className="nodeClassname main12 nodeItem "
@@ -421,15 +331,6 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
/>
</div>
</div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={
@@ -549,15 +450,6 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
label
</span>
</div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={

View File

@@ -20,7 +20,7 @@
}
&.showingMenu {
background-color: #eee;
background-color: #EEE;
}
.treeMenuEllipsis {
@@ -78,12 +78,3 @@
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 DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
@@ -1424,40 +1424,71 @@ export default class Explorer {
// TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer();
this._setLoadingStatusText("Fetching databases...");
readDatabases().then(
(databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess(
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
},
startKey
);
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();
const refreshDatabases = (offers?: DataModels.Offer[]) => {
this._setLoadingStatusText("Fetching databases...");
readDatabases().then(
(databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess(
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
},
reason => {
this._setLoadingStatusText("Failed to fetch containers.");
deferred.reject(reason);
}
)
.finally(() => this.isRefreshingExplorer(false));
startKey
);
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
const deltaDatabases = this.getDeltaDatabases(databases, offers);
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 => {
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 => {
this._setLoadingStatusText("Failed to fetch databases.");
this._setLoadingStatusText("Failed to fetch offers.");
this.isRefreshingExplorer(false);
deferred.reject(error);
TelemetryProcessor.traceFailure(
@@ -2072,13 +2103,16 @@ export default class Explorer {
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
});
databasesToLoad.forEach(async (database: ViewModels.Database) => {
await database.loadCollections();
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
if (isNewDatabase) {
database.expandDatabase();
}
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
databasesToLoad.forEach((database: ViewModels.Database) => {
loadCollectionPromises.push(
database.loadCollections().finally(() => {
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
if (isNewDatabase) {
database.expandDatabase();
}
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
})
);
});
Q.all(loadCollectionPromises).done(
@@ -2223,7 +2257,8 @@ export default class Explorer {
}
private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[]
updatedDatabaseList: DataModels.Database[],
updatedOffersList: DataModels.Offer[]
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } {
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some(
@@ -2232,9 +2267,10 @@ export default class Explorer {
);
return !databaseExists;
});
const databasesToAdd: ViewModels.Database[] = newDatabases.map(
(newDatabase: DataModels.Database) => new Database(this, newDatabase)
);
const databasesToAdd: ViewModels.Database[] = _.map(newDatabases, (newDatabase: DataModels.Database) => {
const databaseOffer: DataModels.Offer = this.getOfferForResource(updatedOffersList, newDatabase._self);
return new Database(this, newDatabase, databaseOffer);
});
let databasesToDelete: ViewModels.Database[] = [];
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
@@ -2284,6 +2320,10 @@ export default class Explorer {
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> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
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;
}
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 {
const label = "New Notebook";
return {

View File

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

View File

@@ -3,7 +3,6 @@ import "./base.css";
import "./default.css";
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import { AzureTheme } from "./AzureTheme";
import { connect } from "react-redux";
@@ -16,7 +15,6 @@ import "./NotebookReadOnlyRenderer.less";
export interface NotebookRendererProps {
contentRef: any;
hideInputs?: boolean;
hidePrompts?: boolean;
}
interface PassedEditorProps {
@@ -40,29 +38,6 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
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 {
return (
<div className="NotebookReadOnlyRender">
@@ -71,7 +46,6 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
<CodeCell id={id} contentRef={contentRef}>
{{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
editor: {
codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />

View File

@@ -115,10 +115,10 @@
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<div class="databaseProvision" aria-label="Provision database throughput"
<div class="databaseProvision" aria-label="New database provision support"
data-bind="visible: databaseCreateNew">
<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" />
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
@@ -517,13 +517,13 @@
<div>
<span class="mandatoryStar">*</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">
<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
data, without impacting the performance of transactional workloads.
Learn more <a id="link1" class="errorLink" href="https://aka.ms/analytical-store-overview"
target="_blank" data-bind="event: { focusout: onFocusOut, keydown: onKeyDown.bind($data, 'largePartitionKey') }">here</a>
Learn more <a class="errorLink" href="https://aka.ms/analytical-store-overview"
target="_blank">here</a>
</span>
</span>
</div>

View File

@@ -666,7 +666,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
const subscriptionType: ViewModels.SubscriptionType =
this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
if (subscriptionType === ViewModels.SubscriptionType.EA) {
return false;
}
@@ -681,7 +681,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true;
};
public async open(databaseId?: string) {
public open(databaseId?: string) {
super.open();
// TODO: Figure out if a database level partition split is about to happen once shared throughput read is available
this.formWarnings("");
@@ -715,40 +715,18 @@ export default class AddCollectionPane extends ContextualPaneBase {
dataExplorerArea: Constants.Areas.ContextualPane
};
await this.container.loadDatabaseOffers();
this._onDatabasesChange(this.container.databases());
this._setFocus();
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[]) {
const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => {
if (database && database.offer && database.offer()) {
this._databaseOffers.set(database.id(), database.offer());
} else if (database && database.isDatabaseShared && database.isDatabaseShared()) {
database.readSettings();
}
return database.id();

View File

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

View File

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

View File

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

View File

@@ -67,7 +67,8 @@
name="collectionIdConfirmation"
required
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>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import { AuthType } from "../../AuthType";
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import * as Constants from "../../Common/Constants";
import * as Entities from "./Entities";
import EnvironmentUtility from "../../Common/EnvironmentUtility";
import * as HeadersUtility from "../../Common/HeadersUtility";
import * as Logger from "../../Common/Logger";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
@@ -307,7 +308,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestQueryApi
: Constants.CassandraBackend.queryApi;
$.ajax(`${collection.container.extensionEndpoint()}${apiEndpoint}`, {
$.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`, {
type: "POST",
data: {
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
@@ -558,7 +559,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestKeysApi
: Constants.CassandraBackend.keysApi;
let endpoint = `${collection.container.extensionEndpoint()}${apiEndpoint}`;
let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`;
const deferred = Q.defer<CassandraTableKeys>();
$.ajax(endpoint, {
type: "POST",
@@ -613,7 +614,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestSchemaApi
: Constants.CassandraBackend.schemaApi;
let endpoint = `${collection.container.extensionEndpoint()}${apiEndpoint}`;
let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`;
const deferred = Q.defer<CassandraTableKey[]>();
$.ajax(endpoint, {
type: "POST",
@@ -667,7 +668,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestCreateOrDeleteApi
: Constants.CassandraBackend.createOrDeleteApi;
$.ajax(`${explorer.extensionEndpoint()}${apiEndpoint}`, {
$.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(explorer)}${apiEndpoint}`, {
type: "POST",
data: {
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._setBaseline();
this.database.readSettings();
TelemetryProcessor.traceSuccess(
Action.UpdateSettings,
{
@@ -642,9 +643,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
};
public onActivate(): Q.Promise<any> {
return super.onActivate().then(async () => {
return super.onActivate().then(() => {
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 ViewModels from "../../Contracts/ViewModels";
import AuthHeadersUtil from "../../Platform/Hosted/Authorization";
import EnvironmentUtility from "../../Common/EnvironmentUtility";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import Q from "q";
import TabsBase from "./TabsBase";
@@ -108,7 +109,11 @@ export default class MongoShellTab extends TabsBase {
) + Constants.MongoDBAccounts.defaultPort.toString();
const databaseId = this.collection.databaseId;
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;
shellIframe.contentWindow.postMessage(
@@ -137,7 +142,7 @@ export default class MongoShellTab extends TabsBase {
return;
}
const dataToLog = { message: event.data.data.logData };
const dataToLog: string = event.data.data.logData;
const logType: string = event.data.data.logType;
const shellTraceId: string = event.data.data.traceId || "none";

View File

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

View File

@@ -1270,10 +1270,8 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
}
public onActivate(): Q.Promise<any> {
return super.onActivate().then(async () => {
return super.onActivate().then(() => {
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 * as Logger from "../../Common/Logger";
import Explorer from "../Explorer";
import { readOffers, readOffer } from "../../Common/DocumentClientUtilityBase";
import { readCollections } from "../../Common/dataAccess/readCollections";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
export default class Database implements ViewModels.Database {
public nodeKind: string;
@@ -27,13 +27,13 @@ export default class Database implements ViewModels.Database {
public isDatabaseShared: ko.Computed<boolean>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
constructor(container: Explorer, data: any) {
constructor(container: Explorer, data: any, offer: DataModels.Offer) {
this.nodeKind = "Database";
this.container = container;
this.self = data._self;
this.rid = data._rid;
this.id = ko.observable(data.id);
this.offer = ko.observable();
this.offer = ko.observable(offer);
this.collections = ko.observableArray<Collection>();
this.isDatabaseExpanded = ko.observable<boolean>(false);
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
@@ -66,7 +66,7 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Scale"
});
pendingNotificationsPromise.then(
Q.all([pendingNotificationsPromise, this.readSettings()]).then(
(data: any) => {
const pendingNotification: DataModels.Notification = data && data[0];
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 {
return (
!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()) {
return;
}
await this.loadOffer();
await this.loadCollections();
this.loadCollections();
this.isDatabaseExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Database node",
@@ -175,19 +259,32 @@ export default class Database implements ViewModels.Database {
});
}
public async loadCollections(): Promise<void> {
const collectionVMs: Collection[] = [];
const collections: DataModels.Collection[] = await readCollections(this.id());
const deltaCollections = this.getDeltaCollections(collections);
public loadCollections(): Q.Promise<void> {
let collectionVMs: Collection[] = [];
let deferred: Q.Deferred<void> = Q.defer<void>();
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
collectionVMs.push(collectionVM);
});
readCollections(this.id()).then(
(collections: DataModels.Collection[]) => {
let collectionsToAddVMPromises: Q.Promise<any>[] = [];
let deltaCollections = this.getDeltaCollections(collections);
//merge collections
this.addCollectionsToList(collectionVMs);
this.deleteCollectionsFromList(deltaCollections.toDelete);
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
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) {
@@ -199,16 +296,6 @@ export default class Database implements ViewModels.Database {
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> {
if (!this.container) {
return Q.resolve(undefined);
@@ -289,10 +376,6 @@ export default class Database implements ViewModels.Database {
}
private deleteCollectionsFromList(collectionsToRemove: Collection[]): void {
if (collectionsToRemove.length === 0) {
return;
}
const collectionsToKeep: Collection[] = [];
ko.utils.arrayForEach(this.collections(), (collection: Collection) => {
@@ -304,4 +387,8 @@ export default class Database implements ViewModels.Database {
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[]) => {
// Clean up old databases
this.cleanupDatabasesKoSubs();
this.cleanupDatabasesKoSubs(databases.map((database: ViewModels.Database) => database.id()));
databases.forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.triggerRender();
@@ -170,17 +170,14 @@ export class ResourceTreeAdapter implements ReactAdapter {
children: [],
isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database),
onClick: async isExpanded => {
onClick: isExpanded => {
// Rewritten version of expandCollapseDatabase():
if (isExpanded) {
database.collapseDatabase();
if (!isExpanded) {
database.expandDatabase();
database.loadCollections();
} else {
if (databaseNode.children?.length === 0) {
databaseNode.isLoading = true;
}
await database.expandDatabase();
database.collapseDatabase();
}
databaseNode.isLoading = false;
database.selectDatabase();
this.container.onUpdateTabsButtons([]);
this.container.tabsManager.refreshActiveTab(
@@ -206,12 +203,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
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;
});
@@ -799,10 +790,16 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.koSubsCollectionIdMap.push(collectionId, sub);
}
private cleanupDatabasesKoSubs(): void {
this.koSubsDatabaseIdMap.keys().forEach((databaseId: string) => {
this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose());
this.koSubsDatabaseIdMap.delete(databaseId);
private cleanupDatabasesKoSubs(existingDatabaseIds: string[]): void {
const databaseIdsToRemove = this.databaseCollectionIdMap
.keys()
.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)) {
this.databaseCollectionIdMap

View File

@@ -35,19 +35,13 @@ const onInit = async () => {
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
galleryItem = galleryItemJunoResponse.data;
}
// 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);
render(notebookUrl, backNavigationText, hideInputs, galleryItem, onBackClick);
};
const render = (
notebookUrl: string,
backNavigationText: string,
hideInputs?: boolean,
hidePrompts?: boolean,
hideInputs: boolean,
galleryItem?: IGalleryItem,
onBackClick?: () => void
) => {
@@ -57,7 +51,6 @@ const render = (
galleryItem,
backNavigationText,
hideInputs,
hidePrompts,
onBackClick: onBackClick,
onTagClick: undefined
};

View File

@@ -4,15 +4,12 @@ import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { appInsights } from "../appInsights";
import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext";
import { getDataExplorerWindow } from "../../Utils/WindowUtils";
/**
* Class that persists telemetry data to the portal tables.
*/
type TelemetryData = { [key: string]: unknown };
export function trace(action: Action, actionModifier: string = ActionModifiers.Mark, data?: TelemetryData): void {
export function trace(action: Action, actionModifier: string = ActionModifiers.Mark, data?: unknown): void {
sendMessage({
type: MessageTypes.TelemetryInfo,
data: {
@@ -25,7 +22,7 @@ export function trace(action: Action, actionModifier: string = ActionModifiers.M
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();
sendMessage({
type: MessageTypes.TelemetryInfo,
@@ -41,7 +38,7 @@ export function traceStart(action: Action, data?: TelemetryData): number {
return timestamp;
}
export function traceSuccess(action: Action, data?: TelemetryData, timestamp?: number): void {
export function traceSuccess(action: Action, data?: unknown, timestamp?: number): void {
sendMessage({
type: MessageTypes.TelemetryInfo,
data: {
@@ -55,7 +52,7 @@ export function traceSuccess(action: Action, data?: TelemetryData, timestamp?: n
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({
type: MessageTypes.TelemetryInfo,
data: {
@@ -69,7 +66,7 @@ export function traceFailure(action: Action, data?: TelemetryData, timestamp?: n
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({
type: MessageTypes.TelemetryInfo,
data: {
@@ -83,7 +80,7 @@ export function traceCancel(action: Action, data?: TelemetryData, timestamp?: nu
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();
sendMessage({
type: MessageTypes.TelemetryInfo,
@@ -99,7 +96,7 @@ export function traceOpen(action: Action, data?: TelemetryData, timestamp?: numb
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();
sendMessage({
type: MessageTypes.TelemetryInfo,
@@ -115,16 +112,21 @@ export function traceMark(action: Action, data?: TelemetryData, timestamp?: numb
return validTimestamp;
}
function getData(actionModifier: string, data: TelemetryData = {}): { [key: string]: string } {
const dataExplorerWindow = getDataExplorerWindow(window);
return {
// 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
authType: dataExplorerWindow && (dataExplorerWindow as any).authType,
subscriptionId: userContext.subscriptionId as string,
platform: configContext.platform,
env: process.env.NODE_ENV as string,
actionModifier,
...data
};
function getData(actionModifier: string, data: unknown = {}): { [key: string]: string } | undefined {
if (typeof data === "string") {
data = { message: data };
}
if (typeof data === "object") {
return {
// 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
authType: (window as any).authType,
subscriptionId: userContext.subscriptionId as string,
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 { Action } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../UserContext";
import { TerminalQueryParams } from "../Common/Constants";
const getUrlVars = (): { [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 => {
let body: BodyInit;
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
if (urlVars.hasOwnProperty("terminalEndpoint")) {
body = JSON.stringify({
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint]
endpoint: urlVars["terminalEndpoint"]
});
}
const server = urlVars[TerminalQueryParams.Server];
const server = urlVars["server"];
let options: Partial<ServerConnection.ISettings> = {
baseUrl: server,
init: { body },
fetch: window.parent.fetch
};
if (urlVars.hasOwnProperty(TerminalQueryParams.Token)) {
if (urlVars.hasOwnProperty("token")) {
options = {
baseUrl: server,
token: urlVars[TerminalQueryParams.Token],
token: urlVars["token"],
init: { body },
fetch: window.parent.fetch
};
@@ -46,12 +44,6 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect
const main = async (): Promise<void> => {
const urlVars = getUrlVars();
// Initialize userContext. Currently only subscrptionId is required by TelemetryProcessor
updateUserContext({
subscriptionId: urlVars[TerminalQueryParams.SubscriptionId]
});
const serverSettings = createServerSettings(urlVars);
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, {
@@ -59,7 +51,7 @@ const main = async (): Promise<void> => {
});
try {
if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) {
if (urlVars.hasOwnProperty("terminal")) {
await JupyterLabAppFactory.createTerminalApp(serverSettings);
} else {
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);
}
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
function isValidOrigin(allowedOrigins: RegExp, event: MessageEvent): boolean {
const eventOrigin = (event && event.origin) || "";
const windowOrigin = (window && window.origin) || "";
if (eventOrigin === windowOrigin) {
return true;
}
for (const origin of allowedOrigins) {
const result = new RegExp(origin).test(eventOrigin);
if (result) {
return true;
}
}
return false;
const result = allowedOrigins && allowedOrigins.test(eventOrigin);
return result;
}

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 crypto from "crypto";
import crypto from 'crypto'
jest.setTimeout(300000);
describe("Collection Add and Delete SQL spec", () => {
it("creates a collection", async () => {
describe('Collection Add and Delete SQL spec', () => {
it('creates a collection', async () => {
try {
const dbId = `TestDatabase${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);
// log in with connection string
const handle = await page.waitForSelector("iframe");
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");
await frame.waitFor('div > p.switchConnectTypeText', { visible: true });
await frame.click('div > p.switchConnectTypeText');
const connStr = process.env.PORTAL_RUNNER_CONNECTION_STRING;
await frame.type("input[class='inputToken']", connStr);
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.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.click('button[data-test="New Container"]');
// check new database
await frame.waitFor('input[data-test="addCollection-createNewDatabase"]');
await frame.click('input[data-test="addCollection-createNewDatabase"]');
// check shared throughput
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
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
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
await frame.waitFor('input[data-test="addCollection-partitionKeyValue"]');
await frame.type('input[data-test="addCollection-partitionKeyValue"]', sharedKey);
// click submit
await frame.waitFor("#submitBtnAddCollection");
await frame.click("#submitBtnAddCollection");
await frame.waitFor('#submitBtnAddCollection');
await frame.click('#submitBtnAddCollection');
// validate created
// 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.click(`div[data-test="${dbId}"]`);
await frame.waitFor(3000);
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
await frame.waitFor(`span[title="${collectionId}"]`);
// delete container
// click context menu for container
await frame.waitFor(`div[data-test="${collectionId}"] > div > button`, { visible: true });
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
await frame.waitFor(`div[data-test="${collectionId}"] > div > button`);
await frame.click(`div[data-test="${collectionId}"] > div > button`);
await frame.waitFor(2000);
// click delete container
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]', { visible: true });
await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
await frame.waitForSelector('body > div.ms-Layer.ms-Layer--fixed');
await frame.waitFor(1000);
const elements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
await elements[4].click();
// confirm delete container
await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true });
await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim());
// click delete
await frame.waitFor('input[data-test="deleteCollection"]', { visible: true });
await frame.click('input[data-test="deleteCollection"]');
await frame.waitFor(5000);
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();
// click delete database
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
await frame.waitFor(1000);
const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
await dbElements[1].click();
// confirm delete database
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 expect(page).not.toMatchElement(`div[data-test="${dbId}"]`);
} 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}.jpg` });
await page.screenshot({path: 'failure.png'});
throw error;
}
});
});
}
})
})

View File

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

View File

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