mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-28 21:32:05 +00:00
Compare commits
21 Commits
users/srna
...
exclude-ve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6e4d1eaf9 | ||
|
|
2d3d96bcc7 | ||
|
|
f2d4cfcef9 | ||
|
|
70c7d84bdb | ||
|
|
dcc2036793 | ||
|
|
987368fe58 | ||
|
|
9ea588261e | ||
|
|
91aa91d860 | ||
|
|
2e747a1a07 | ||
|
|
290ca4aba5 | ||
|
|
28ceb18d73 | ||
|
|
666a378b3b | ||
|
|
13dafb9581 | ||
|
|
7c5c8ddb7a | ||
|
|
3f2c67af23 | ||
|
|
3ae1f97ccc | ||
|
|
92c4440d38 | ||
|
|
1ccffab911 | ||
|
|
dc56f7e154 | ||
|
|
e62184a1f2 | ||
|
|
26c832437b |
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -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/')
|
||||
|
||||
@@ -5,6 +5,7 @@ module.exports = {
|
||||
headless: isCI,
|
||||
slowMo: 50,
|
||||
defaultViewport: null,
|
||||
ignoreHTTPSErrors: true
|
||||
ignoreHTTPSErrors: true,
|
||||
args: ["--disable-web-security"]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
87
src/Common/dataAccess/readDatabaseOffer.ts
Normal file
87
src/Common/dataAccess/readDatabaseOffer.ts
Normal 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;
|
||||
};
|
||||
32
src/Common/dataAccess/readOffers.ts
Normal file
32
src/Common/dataAccess/readOffers.ts
Normal 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;
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))}&`;
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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="" />
|
||||
}))
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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("");
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
49
src/Utils/WindowUtils.test.ts
Normal file
49
src/Utils/WindowUtils.test.ts
Normal 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
23
src/Utils/WindowUtils.ts
Normal 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;
|
||||
};
|
||||
98
test/cassandra/container.spec.ts
Normal file
98
test/cassandra/container.spec.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"sourceMap": false,
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user