Compare commits

...

21 Commits

Author SHA1 Message Date
Steve Faulkner
a6e4d1eaf9 Remove default 2020-09-25 18:13:11 -05:00
Steve Faulkner
2d3d96bcc7 xMerge branch 'master' into exclude-vendor-from-sourcemaps 2020-09-25 18:12:17 -05:00
Steve Faulkner
f2d4cfcef9 Fixes 2020-09-25 18:12:04 -05:00
Tanuj Mittal
70c7d84bdb Do not fail when trying to find DE window with cross origin (#231)
* Do not fail when trying to find DE window with cross origin

* Fix lint errors
2020-09-25 14:49:11 -07:00
Tanuj Mittal
dcc2036793 Fix hotfix syntax in workflow (#232) 2020-09-25 14:44:15 -07:00
Tanuj Mittal
987368fe58 Disable endtoendpuppeteer tests (temporarily) (#233) 2020-09-25 14:38:49 -07:00
Steve Faulkner
9ea588261e Don't generate source maps for vendor files 2020-09-25 14:46:51 -05:00
Steve Faulkner
91aa91d860 Cleanup extension endpoint loading (#224) 2020-09-24 18:10:54 -05:00
victor-meng
2e747a1a07 Fix create database for serverless accounts (#228) 2020-09-24 14:03:37 -07:00
Zachary Foster
290ca4aba5 Commits add table entity pane changes without steve's changes (#227) 2020-09-23 18:29:04 -04:00
victor-meng
28ceb18d73 Use SDK for reading database offer for Tables API (#226) 2020-09-23 13:32:47 -07:00
victor-meng
666a378b3b Fix resource tree refresh issue (#222) 2020-09-23 13:18:05 -07:00
Zachary Foster
13dafb9581 Adds cassandra e2e container CRUD test (#195)
* Starts cassandra

* Adds more cassandra

* wip

* Fix a few extra console.logs

* Format

* Adds cassandra connection string secret to ci

* Adds test name to failure screenshot

* Disable no-any on expect type, as it has getState() method

* Constantize some delays

* Accidentally deleted a brace
2020-09-22 16:21:57 -04:00
Tanuj Mittal
7c5c8ddb7a Fix incorrect usage of TelemetryProcessor (#221)
* Fix incorrect usage of TelemetryProcessor

* Address feedback
2020-09-22 12:19:06 -07:00
victor-meng
3f2c67af23 Fix some content becomes hidden after zooming to 200% (#223) 2020-09-22 11:59:44 -07:00
artrejo
3ae1f97ccc Remove AFEC check for Synapse Link and Mongo 2020-09-21 13:44:05 -07:00
Tanuj Mittal
92c4440d38 Skip refreshing pinned repos if token not available (#219) 2020-09-21 11:41:48 -07:00
victor-meng
1ccffab911 [Accessibility] Add horizontal scroll bar if tooltip box goes outside of panel boundary (#215) 2020-09-18 17:01:12 -07:00
victor-meng
dc56f7e154 Lazy load database offer in data explorer (#208)
Co-authored-by: zfoster <notzachfoster@gmail.com>
2020-09-18 16:00:21 -07:00
Laurent Nguyen
e62184a1f2 Fix inaccessible link by keyboard nav in info bubble (#216)
* Fix inaccessible link by keyboard nav in info bubble

* Fix shift-tab on link to focus previous element
2020-09-18 16:26:04 +02:00
Laurent Nguyen
26c832437b Support showing only notebook output scenario (#177)
* Enable hiding prompt when hiding inputs (so that only output is showing)

* Fix format

* Rename variable

* Use nteract prompt instead of copying source
2020-09-18 16:25:09 +02:00
49 changed files with 786 additions and 592 deletions

View File

@@ -3,8 +3,8 @@ on:
push:
branches:
- master
- hotfix/*
- release/*
- hotfix/**
- release/**
pull_request:
branches:
- master
@@ -196,26 +196,6 @@ jobs:
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
endtoendpuppeteer:
name: "End to end puppeteer tests"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: End to End Puppeteer Tests
run: |
npm ci
npm start &
npm run wait-for-server
npm run test:e2e
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
nuget:
name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')

View File

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

View File

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

View File

@@ -170,89 +170,8 @@ 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";
@@ -562,3 +481,11 @@ 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,49 +1,8 @@
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,34 +25,4 @@ 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,6 +2,7 @@ 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>;
@@ -48,38 +49,18 @@ export function sendCachedDataMessage<TResponseDataModel>(
export function sendMessage(data: any): void {
if (canSendMessage()) {
const dataExplorerWindow = getDataExplorerWindow(window);
if (dataExplorerWindow) {
dataExplorerWindow.parent.postMessage(
{
signature: "pcIframe",
data: data
},
dataExplorerWindow.document.referrer
);
}
// We try to find data explorer window first, then fallback to current window
const portalChildWindow = getDataExplorerWindow(window) || window;
portalChildWindow.parent.postMessage(
{
signature: "pcIframe",
data: data
},
portalChildWindow.document.referrer
);
}
}
// 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;
// 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;
}
}
return dataExplorerWindow;
};
export function canSendMessage(): boolean {
return window.parent !== window;
}

View File

@@ -1,9 +1,8 @@
import { AuthType } from "../AuthType";
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
import { 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");
@@ -237,19 +236,19 @@ describe("MongoProxyClient", () => {
});
it("returns a production endpoint", () => {
const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
const endpoint = getEndpoint();
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(databaseAccount as DatabaseAccount);
const endpoint = getEndpoint();
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
});
it("returns a guest endpoint", () => {
window.authType = AuthType.EncryptedToken;
const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
const endpoint = getEndpoint();
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
});
});

View File

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

View File

@@ -0,0 +1,87 @@
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

@@ -0,0 +1,32 @@
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

@@ -289,6 +289,12 @@ 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(): void;
expandDatabase(): Promise<void>;
collapseDatabase(): void;
loadCollections(): Q.Promise<void>;
loadCollections(): 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,7 +42,8 @@ export class ResourceTreeContextMenuButtonFactory {
const deleteDatabaseMenuItem = {
iconSrc: DeleteDatabaseIcon,
onClick: () => container.deleteDatabaseConfirmationPane.open(),
label: container.deleteDatabaseText()
label: container.deleteDatabaseText(),
styleClass: "deleteDatabaseMenuItem"
};
return [newCollectionMenuItem, deleteDatabaseMenuItem];
}
@@ -112,7 +113,8 @@ export class ResourceTreeContextMenuButtonFactory {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
},
label: container.deleteCollectionText()
label: container.deleteCollectionText(),
styleClass: "deleteCollectionMenuItem"
});
return items;

View File

@@ -8,6 +8,8 @@ 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;
@@ -32,11 +34,11 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
public getTerminalParams(): Map<string, string> {
let params: Map<string, string> = new Map<string, string>();
params.set("terminal", "true");
params.set(TerminalQueryParams.Terminal, "true");
const terminalEndpoint: string = this.tryGetTerminalEndpoint();
if (terminalEndpoint) {
params.set("terminalEndpoint", terminalEndpoint);
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
}
return params;
@@ -75,11 +77,13 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
return "";
}
params.set("server", serverInfo.notebookServerEndpoint);
params.set(TerminalQueryParams.Server, serverInfo.notebookServerEndpoint);
if (serverInfo.authToken && serverInfo.authToken.length > 0) {
params.set("token", serverInfo.authToken);
params.set(TerminalQueryParams.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,6 +30,7 @@ export interface NotebookViewerComponentProps {
isFavorite?: boolean;
backNavigationText: string;
hideInputs?: boolean;
hidePrompts?: boolean;
onBackClick: () => void;
onTagClick: (tag: string) => void;
}
@@ -148,7 +149,8 @@ export class NotebookViewerComponent extends React.Component<
{this.state.showProgressBar && <ProgressIndicator />}
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
hideInputs: this.props.hideInputs
hideInputs: this.props.hideInputs,
hidePrompts: this.props.hidePrompts
})}
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}

View File

@@ -159,4 +159,20 @@ 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,12 +17,14 @@ 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 {
@@ -37,6 +39,7 @@ 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;
@@ -183,6 +186,9 @@ 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}>
@@ -256,13 +262,20 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
>
{props.item.onRenderIcon()}
<span className="treeComponentMenuItemLabel">{props.item.text}</span>
<span
className={
"treeComponentMenuItemLabel" + (props.item.className ? ` ${props.item.className}Label` : "")
}
>
{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,6 +63,15 @@ 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={
@@ -179,6 +188,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
"isBeakVisible": false,
"items": Array [
Object {
"className": undefined,
"disabled": true,
"key": "menuLabel",
"onClick": undefined,
@@ -201,6 +211,15 @@ 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={
@@ -261,6 +280,77 @@ 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 "
@@ -331,6 +421,15 @@ 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={
@@ -450,6 +549,15 @@ 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,3 +78,12 @@
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 { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
@@ -1424,71 +1424,40 @@ export default class Explorer {
// TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer();
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
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();
},
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);
reason => {
this._setLoadingStatusText("Failed to fetch containers.");
deferred.reject(reason);
}
)
.finally(() => this.isRefreshingExplorer(false));
},
error => {
this._setLoadingStatusText("Failed to fetch offers.");
this._setLoadingStatusText("Failed to fetch databases.");
this.isRefreshingExplorer(false);
deferred.reject(error);
TelemetryProcessor.traceFailure(
@@ -2103,16 +2072,13 @@ export default class Explorer {
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
});
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);
})
);
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);
});
Q.all(loadCollectionPromises).done(
@@ -2257,8 +2223,7 @@ export default class Explorer {
}
private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[],
updatedOffersList: DataModels.Offer[]
updatedDatabaseList: DataModels.Database[]
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } {
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some(
@@ -2267,10 +2232,9 @@ export default class Explorer {
);
return !databaseExists;
});
const databasesToAdd: ViewModels.Database[] = _.map(newDatabases, (newDatabase: DataModels.Database) => {
const databaseOffer: DataModels.Offer = this.getOfferForResource(updatedOffersList, newDatabase._self);
return new Database(this, newDatabase, databaseOffer);
});
const databasesToAdd: ViewModels.Database[] = newDatabases.map(
(newDatabase: DataModels.Database) => new Database(this, newDatabase)
);
let databasesToDelete: ViewModels.Database[] = [];
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
@@ -2320,10 +2284,6 @@ 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";
@@ -3160,4 +3120,15 @@ 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,31 +391,6 @@ 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,11 +113,14 @@ export default class NotebookManager {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
this.params.resourceTree.triggerRender();
});
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
this.refreshPinnedRepos();
}
public refreshPinnedRepos(): void {
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
const token = this.gitHubOAuthService.getTokenObservable()();
if (token) {
this.junoClient.getPinnedRepos(token.scope);
}
}
public async openPublishNotebookPane(

View File

@@ -3,6 +3,7 @@ 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";
@@ -15,6 +16,7 @@ import "./NotebookReadOnlyRenderer.less";
export interface NotebookRendererProps {
contentRef: any;
hideInputs?: boolean;
hidePrompts?: boolean;
}
interface PassedEditorProps {
@@ -38,6 +40,29 @@ 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">
@@ -46,6 +71,7 @@ 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

@@ -517,13 +517,13 @@
<div>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Analytical store</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<span class="infoTooltip" role="tooltip" tabindex="0" data-bind="event: { focus: function(data, event) { transferFocus('tooltip1', 'link1') } }">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext infoTooltipWidth">
<span id="tooltip1" class="tooltiptext infoTooltipWidth" data-bind="event: { mouseout: onMouseOut }">
Enable analytical store capability to perform near real-time analytics on your operational
data, without impacting the performance of transactional workloads.
Learn more <a class="errorLink" href="https://aka.ms/analytical-store-overview"
target="_blank">here</a>
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>
</span>
</span>
</div>

View File

@@ -608,7 +608,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true;
}
if (this.container.isPreferredApiMongoDB() && this.container.hasStorageAnalyticsAfecFeature()) {
if (this.container.isPreferredApiMongoDB()) {
return true;
}
@@ -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) {
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
}
@@ -681,7 +681,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true;
};
public open(databaseId?: string) {
public async 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,18 +715,40 @@ 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) {
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
}

View File

@@ -268,8 +268,6 @@ 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

@@ -134,11 +134,9 @@ 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,
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
)
telemetryProcessorSpy.calledWith(Action.DeleteCollection, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
})
).toBe(true);
});
});

View File

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

View File

@@ -120,11 +120,9 @@ 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,
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
);
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
});
});
});
});

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ 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";
@@ -308,7 +307,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestQueryApi
: Constants.CassandraBackend.queryApi;
$.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`, {
$.ajax(`${collection.container.extensionEndpoint()}${apiEndpoint}`, {
type: "POST",
data: {
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
@@ -559,7 +558,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestKeysApi
: Constants.CassandraBackend.keysApi;
let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`;
let endpoint = `${collection.container.extensionEndpoint()}${apiEndpoint}`;
const deferred = Q.defer<CassandraTableKeys>();
$.ajax(endpoint, {
type: "POST",
@@ -614,7 +613,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestSchemaApi
: Constants.CassandraBackend.schemaApi;
let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`;
let endpoint = `${collection.container.extensionEndpoint()}${apiEndpoint}`;
const deferred = Q.defer<CassandraTableKey[]>();
$.ajax(endpoint, {
type: "POST",
@@ -668,7 +667,7 @@ export class CassandraAPIDataClient extends TableDataClient {
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestCreateOrDeleteApi
: Constants.CassandraBackend.createOrDeleteApi;
$.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(explorer)}${apiEndpoint}`, {
$.ajax(`${explorer.extensionEndpoint()}${apiEndpoint}`, {
type: "POST",
data: {
accountName: explorer.databaseAccount() && explorer.databaseAccount().name,

View File

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

View File

@@ -2,7 +2,6 @@ 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";
@@ -109,11 +108,7 @@ export default class MongoShellTab extends TabsBase {
) + Constants.MongoDBAccounts.defaultPort.toString();
const databaseId = this.collection.databaseId;
const collectionId = this.collection.id();
const apiEndpoint = EnvironmentUtility.getMongoBackendEndpoint(
this._container.serverId(),
userContext.databaseAccount.location,
this._container.extensionEndpoint()
).replace("/api/mongo/explorer", "");
const apiEndpoint = this._container.extensionEndpoint();
const encryptedAuthToken: string = userContext.accessToken;
shellIframe.contentWindow.postMessage(
@@ -142,7 +137,7 @@ export default class MongoShellTab extends TabsBase {
return;
}
const dataToLog: string = event.data.data.logData;
const dataToLog = { message: event.data.data.logData };
const logType: string = event.data.data.logType;
const shellTraceId: string = event.data.data.traceId || "none";

View File

@@ -346,7 +346,6 @@ 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"
@@ -507,7 +506,6 @@ describe("Settings tab", () => {
}
}
};
const database = new Database(explorer, baseDatabase, null);
const container: DataModels.Collection = {
_rid: "_rid",
_self: "",

View File

@@ -1270,8 +1270,10 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
}
public onActivate(): Q.Promise<any> {
return super.onActivate().then(() => {
return super.onActivate().then(async () => {
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, offer: DataModels.Offer) {
constructor(container: Explorer, data: any) {
this.nodeKind = "Database";
this.container = container;
this.self = data._self;
this.rid = data._rid;
this.id = ko.observable(data.id);
this.offer = ko.observable(offer);
this.offer = ko.observable();
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"
});
Q.all([pendingNotificationsPromise, this.readSettings()]).then(
pendingNotificationsPromise.then(
(data: any) => {
const pendingNotification: DataModels.Notification = data && data[0];
settingsTab = new DatabaseSettingsTab({
@@ -121,80 +121,6 @@ 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() &&
@@ -219,23 +145,13 @@ export default class Database implements ViewModels.Database {
});
}
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() {
public async expandDatabase() {
if (this.isDatabaseExpanded()) {
return;
}
this.loadCollections();
await this.loadOffer();
await this.loadCollections();
this.isDatabaseExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Database node",
@@ -259,32 +175,19 @@ export default class Database implements ViewModels.Database {
});
}
public loadCollections(): Q.Promise<void> {
let collectionVMs: Collection[] = [];
let deferred: Q.Deferred<void> = Q.defer<void>();
public async loadCollections(): Promise<void> {
const collectionVMs: Collection[] = [];
const collections: DataModels.Collection[] = await readCollections(this.id());
const deltaCollections = this.getDeltaCollections(collections);
readCollections(this.id()).then(
(collections: DataModels.Collection[]) => {
let collectionsToAddVMPromises: Q.Promise<any>[] = [];
let deltaCollections = this.getDeltaCollections(collections);
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
collectionVMs.push(collectionVM);
});
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;
//merge collections
this.addCollectionsToList(collectionVMs);
this.deleteCollectionsFromList(deltaCollections.toDelete);
}
public openAddCollection(database: Database, event: MouseEvent) {
@@ -296,6 +199,16 @@ 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);
@@ -376,6 +289,10 @@ 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) => {
@@ -387,8 +304,4 @@ 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(databases.map((database: ViewModels.Database) => database.id()));
this.cleanupDatabasesKoSubs();
databases.forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.triggerRender();
@@ -170,14 +170,17 @@ export class ResourceTreeAdapter implements ReactAdapter {
children: [],
isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database),
onClick: isExpanded => {
onClick: async isExpanded => {
// Rewritten version of expandCollapseDatabase():
if (!isExpanded) {
database.expandDatabase();
database.loadCollections();
} else {
if (isExpanded) {
database.collapseDatabase();
} else {
if (databaseNode.children?.length === 0) {
databaseNode.isLoading = true;
}
await database.expandDatabase();
}
databaseNode.isLoading = false;
database.selectDatabase();
this.container.onUpdateTabsButtons([]);
this.container.tabsManager.refreshActiveTab(
@@ -203,6 +206,12 @@ 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;
});
@@ -790,16 +799,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.koSubsCollectionIdMap.push(collectionId, sub);
}
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);
}
private cleanupDatabasesKoSubs(): void {
this.koSubsDatabaseIdMap.keys().forEach((databaseId: string) => {
this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose());
this.koSubsDatabaseIdMap.delete(databaseId);
if (this.databaseCollectionIdMap.has(databaseId)) {
this.databaseCollectionIdMap

View File

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

View File

@@ -4,12 +4,15 @@ 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.
*/
export function trace(action: Action, actionModifier: string = ActionModifiers.Mark, data?: unknown): void {
type TelemetryData = { [key: string]: unknown };
export function trace(action: Action, actionModifier: string = ActionModifiers.Mark, data?: TelemetryData): void {
sendMessage({
type: MessageTypes.TelemetryInfo,
data: {
@@ -22,7 +25,7 @@ export function trace(action: Action, actionModifier: string = ActionModifiers.M
appInsights.trackEvent({ name: Action[action] }, getData(actionModifier, data));
}
export function traceStart(action: Action, data?: unknown): number {
export function traceStart(action: Action, data?: TelemetryData): number {
const timestamp: number = Date.now();
sendMessage({
type: MessageTypes.TelemetryInfo,
@@ -38,7 +41,7 @@ export function traceStart(action: Action, data?: unknown): number {
return timestamp;
}
export function traceSuccess(action: Action, data?: unknown, timestamp?: number): void {
export function traceSuccess(action: Action, data?: TelemetryData, timestamp?: number): void {
sendMessage({
type: MessageTypes.TelemetryInfo,
data: {
@@ -52,7 +55,7 @@ export function traceSuccess(action: Action, data?: unknown, timestamp?: number)
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Success, data));
}
export function traceFailure(action: Action, data?: unknown, timestamp?: number): void {
export function traceFailure(action: Action, data?: TelemetryData, timestamp?: number): void {
sendMessage({
type: MessageTypes.TelemetryInfo,
data: {
@@ -66,7 +69,7 @@ export function traceFailure(action: Action, data?: unknown, timestamp?: number)
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Failed, data));
}
export function traceCancel(action: Action, data?: unknown, timestamp?: number): void {
export function traceCancel(action: Action, data?: TelemetryData, timestamp?: number): void {
sendMessage({
type: MessageTypes.TelemetryInfo,
data: {
@@ -80,7 +83,7 @@ export function traceCancel(action: Action, data?: unknown, timestamp?: number):
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Cancel, data));
}
export function traceOpen(action: Action, data?: unknown, timestamp?: number): number {
export function traceOpen(action: Action, data?: TelemetryData, timestamp?: number): number {
const validTimestamp = timestamp || Date.now();
sendMessage({
type: MessageTypes.TelemetryInfo,
@@ -96,7 +99,7 @@ export function traceOpen(action: Action, data?: unknown, timestamp?: number): n
return validTimestamp;
}
export function traceMark(action: Action, data?: unknown, timestamp?: number): number {
export function traceMark(action: Action, data?: TelemetryData, timestamp?: number): number {
const validTimestamp = timestamp || Date.now();
sendMessage({
type: MessageTypes.TelemetryInfo,
@@ -112,21 +115,16 @@ export function traceMark(action: Action, data?: unknown, timestamp?: number): n
return validTimestamp;
}
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;
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
};
}

View File

@@ -6,6 +6,8 @@ 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 } = {};
@@ -18,22 +20,22 @@ const getUrlVars = (): { [key: string]: string } => {
const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => {
let body: BodyInit;
if (urlVars.hasOwnProperty("terminalEndpoint")) {
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
body = JSON.stringify({
endpoint: urlVars["terminalEndpoint"]
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint]
});
}
const server = urlVars["server"];
const server = urlVars[TerminalQueryParams.Server];
let options: Partial<ServerConnection.ISettings> = {
baseUrl: server,
init: { body },
fetch: window.parent.fetch
};
if (urlVars.hasOwnProperty("token")) {
if (urlVars.hasOwnProperty(TerminalQueryParams.Token)) {
options = {
baseUrl: server,
token: urlVars["token"],
token: urlVars[TerminalQueryParams.Token],
init: { body },
fetch: window.parent.fetch
};
@@ -44,6 +46,12 @@ 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, {
@@ -51,7 +59,7 @@ const main = async (): Promise<void> => {
});
try {
if (urlVars.hasOwnProperty("terminal")) {
if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) {
await JupyterLabAppFactory.createTerminalApp(serverSettings);
} else {
throw new Error("Only terminal is supported");

View File

@@ -0,0 +1,49 @@
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();
});
});
});

23
src/Utils/WindowUtils.ts Normal file
View File

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,98 @@
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,24 +56,27 @@ 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(`span[title="${collectionId}"]`);
await frame.waitFor(3000);
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
// delete container
// click context menu for container
await frame.waitFor(`div[data-test="${collectionId}"] > div > button`);
await frame.waitFor(`div[data-test="${collectionId}"] > div > button`, { visible: true });
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
await frame.click(`div[data-test="${collectionId}"] > div > button`);
await frame.waitFor(2000);
// click delete container
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();
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]', { visible: true });
await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
// 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 });
@@ -87,9 +90,8 @@ describe('Collection Add and Delete SQL spec', () => {
await button.asElement().click();
// click delete database
await frame.waitFor(1000);
const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
await dbElements[1].click();
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
// confirm delete database
await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim());
@@ -99,8 +101,10 @@ 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) {
await page.screenshot({path: 'failure.png'});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `Test Failed ${testName}.jpg` });
throw error;
}
})
})
}
});
});

View File

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

View File

@@ -11,6 +11,7 @@ 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");
@@ -104,6 +105,15 @@ 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({
@@ -164,7 +174,9 @@ module.exports = function(env = {}, argv = {}) {
new CopyWebpackPlugin({
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) {
@@ -194,7 +206,6 @@ 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
@@ -206,6 +217,7 @@ 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