mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-29 05:41:40 +00:00
Compare commits
26 Commits
hotfix/fix
...
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 | ||
|
|
832f8d560d | ||
|
|
d85c96d408 | ||
|
|
bad6a60d07 | ||
|
|
b690fe18e6 | ||
|
|
1bbe08378c |
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -3,8 +3,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- hotfix/*
|
- hotfix/**
|
||||||
- release/*
|
- release/**
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
@@ -196,26 +196,6 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
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:
|
nuget:
|
||||||
name: Publish Nuget
|
name: Publish Nuget
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module.exports = {
|
|||||||
headless: isCI,
|
headless: isCI,
|
||||||
slowMo: 50,
|
slowMo: 50,
|
||||||
defaultViewport: null,
|
defaultViewport: null,
|
||||||
ignoreHTTPSErrors: true
|
ignoreHTTPSErrors: true,
|
||||||
|
args: ["--disable-web-security"]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1743,7 +1743,7 @@ input::-webkit-calendar-picker-indicator {
|
|||||||
padding-right: 34px;
|
padding-right: 34px;
|
||||||
color: @BaseDark;
|
color: @BaseDark;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: auto;
|
||||||
margin: (2 * @MediumSpace) 0px;
|
margin: (2 * @MediumSpace) 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2078,7 +2078,7 @@ a:link {
|
|||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,89 +170,8 @@ export enum MongoBackendEndpointType {
|
|||||||
remote
|
remote
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MongoBackend {
|
|
||||||
public static localhostEndpoint: string = "/api/mongo/explorer";
|
|
||||||
public static centralUsEndpoint: string = "https://main.documentdb.ext.azure.com/api/mongo/explorer";
|
|
||||||
public static northEuropeEndpoint: string = "https://main.documentdb.ext.azure.com/api/mongo/explorer";
|
|
||||||
public static southEastAsiaEndpoint: string = "https://main.documentdb.ext.azure.com/api/mongo/explorer";
|
|
||||||
|
|
||||||
public static endpointsByRegion: any = {
|
|
||||||
default: MongoBackend.centralUsEndpoint,
|
|
||||||
northeurope: MongoBackend.northEuropeEndpoint,
|
|
||||||
ukwest: MongoBackend.northEuropeEndpoint,
|
|
||||||
uksouth: MongoBackend.northEuropeEndpoint,
|
|
||||||
westeurope: MongoBackend.northEuropeEndpoint,
|
|
||||||
australiaeast: MongoBackend.southEastAsiaEndpoint,
|
|
||||||
australiasoutheast: MongoBackend.southEastAsiaEndpoint,
|
|
||||||
centralindia: MongoBackend.southEastAsiaEndpoint,
|
|
||||||
eastasia: MongoBackend.southEastAsiaEndpoint,
|
|
||||||
japaneast: MongoBackend.southEastAsiaEndpoint,
|
|
||||||
japanwest: MongoBackend.southEastAsiaEndpoint,
|
|
||||||
koreacentral: MongoBackend.southEastAsiaEndpoint,
|
|
||||||
koreasouth: MongoBackend.southEastAsiaEndpoint,
|
|
||||||
southeastasia: MongoBackend.southEastAsiaEndpoint,
|
|
||||||
southindia: MongoBackend.southEastAsiaEndpoint,
|
|
||||||
westindia: MongoBackend.southEastAsiaEndpoint
|
|
||||||
};
|
|
||||||
|
|
||||||
public static endpointsByEnvironment: any = {
|
|
||||||
default: MongoBackendEndpointType.local,
|
|
||||||
localhost: MongoBackendEndpointType.local,
|
|
||||||
prod1: MongoBackendEndpointType.remote,
|
|
||||||
prod2: MongoBackendEndpointType.remote
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
||||||
export class CassandraBackend {
|
export class CassandraBackend {
|
||||||
public static readonly localhostEndpoint: string = "https://localhost:12901/";
|
|
||||||
public static readonly devEndpoint: string = "https://platformproxycassandradev.azurewebsites.net/";
|
|
||||||
|
|
||||||
public static readonly centralUsEndpoint: string = "https://main.documentdb.ext.azure.com/";
|
|
||||||
public static readonly northEuropeEndpoint: string = "https://main.documentdb.ext.azure.com/";
|
|
||||||
public static readonly southEastAsiaEndpoint: string = "https://main.documentdb.ext.azure.com/";
|
|
||||||
|
|
||||||
public static readonly bf_default: string = "https://main.documentdb.ext.microsoftazure.de/";
|
|
||||||
public static readonly mc_default: string = "https://main.documentdb.ext.azure.cn/";
|
|
||||||
public static readonly ff_default: string = "https://main.documentdb.ext.azure.us/";
|
|
||||||
|
|
||||||
public static readonly endpointsByRegion: any = {
|
|
||||||
default: CassandraBackend.centralUsEndpoint,
|
|
||||||
northeurope: CassandraBackend.northEuropeEndpoint,
|
|
||||||
ukwest: CassandraBackend.northEuropeEndpoint,
|
|
||||||
uksouth: CassandraBackend.northEuropeEndpoint,
|
|
||||||
westeurope: CassandraBackend.northEuropeEndpoint,
|
|
||||||
australiaeast: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
australiasoutheast: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
centralindia: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
eastasia: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
japaneast: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
japanwest: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
koreacentral: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
koreasouth: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
southeastasia: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
southindia: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
westindia: CassandraBackend.southEastAsiaEndpoint,
|
|
||||||
|
|
||||||
// Black Forest
|
|
||||||
germanycentral: CassandraBackend.bf_default,
|
|
||||||
germanynortheast: CassandraBackend.bf_default,
|
|
||||||
|
|
||||||
// Fairfax
|
|
||||||
usdodeast: CassandraBackend.ff_default,
|
|
||||||
usdodcentral: CassandraBackend.ff_default,
|
|
||||||
usgovarizona: CassandraBackend.ff_default,
|
|
||||||
usgoviowa: CassandraBackend.ff_default,
|
|
||||||
usgovtexas: CassandraBackend.ff_default,
|
|
||||||
usgovvirginia: CassandraBackend.ff_default,
|
|
||||||
|
|
||||||
// Mooncake
|
|
||||||
chinaeast: CassandraBackend.mc_default,
|
|
||||||
chinaeast2: CassandraBackend.mc_default,
|
|
||||||
chinanorth: CassandraBackend.mc_default,
|
|
||||||
chinanorth2: CassandraBackend.mc_default
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||||
public static readonly queryApi: string = "api/cassandra";
|
public static readonly queryApi: string = "api/cassandra";
|
||||||
@@ -562,3 +481,11 @@ export class AnalyticalStorageTtl {
|
|||||||
public static readonly Infinite: number = -1;
|
public static readonly Infinite: number = -1;
|
||||||
public static readonly Disabled: number = 0;
|
public static readonly Disabled: number = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TerminalQueryParams {
|
||||||
|
public static readonly Terminal = "terminal";
|
||||||
|
public static readonly Server = "server";
|
||||||
|
public static readonly Token = "token";
|
||||||
|
public static readonly SubscriptionId = "subscriptionId";
|
||||||
|
public static readonly TerminalEndpoint = "terminalEndpoint";
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
export default class EnvironmentUtility {
|
||||||
public static getMongoBackendEndpoint(serverId: string, location: string, extensionEndpoint: string = ""): string {
|
|
||||||
const defaultEnvironment: string = "default";
|
|
||||||
const defaultLocation: string = "default";
|
|
||||||
let environment: string = serverId;
|
|
||||||
const endpointType: Constants.MongoBackendEndpointType =
|
|
||||||
Constants.MongoBackend.endpointsByEnvironment[environment] ||
|
|
||||||
Constants.MongoBackend.endpointsByEnvironment[defaultEnvironment];
|
|
||||||
if (endpointType === Constants.MongoBackendEndpointType.local) {
|
|
||||||
return `${extensionEndpoint}${Constants.MongoBackend.localhostEndpoint}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedLocation = EnvironmentUtility.normalizeRegionName(location);
|
|
||||||
return (
|
|
||||||
Constants.MongoBackend.endpointsByRegion[normalizedLocation] ||
|
|
||||||
Constants.MongoBackend.endpointsByRegion[defaultLocation]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static isAadUser(): boolean {
|
|
||||||
return window.authType === AuthType.AAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getCassandraBackendEndpoint(explorer: Explorer): string {
|
|
||||||
const defaultLocation: string = "default";
|
|
||||||
const location: string = EnvironmentUtility.normalizeRegionName(explorer.databaseAccount().location);
|
|
||||||
return (
|
|
||||||
Constants.CassandraBackend.endpointsByRegion[location] ||
|
|
||||||
Constants.CassandraBackend.endpointsByRegion[defaultLocation]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static normalizeArmEndpointUri(uri: string): string {
|
public static normalizeArmEndpointUri(uri: string): string {
|
||||||
if (uri && uri.slice(-1) !== "/") {
|
if (uri && uri.slice(-1) !== "/") {
|
||||||
return `${uri}/`;
|
return `${uri}/`;
|
||||||
}
|
}
|
||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static normalizeRegionName(region: string): string {
|
|
||||||
return region && StringUtils.stripSpacesFromString(region.toLocaleLowerCase());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,34 +25,4 @@ describe("Message Handler", () => {
|
|||||||
MessageHandler.runGarbageCollector();
|
MessageHandler.runGarbageCollector();
|
||||||
expect(MessageHandler.RequestMap["123"]).toBeUndefined();
|
expect(MessageHandler.RequestMap["123"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getDataExplorerWindow", () => {
|
|
||||||
it("should return current window if current window has dataExplorerPlatform property", () => {
|
|
||||||
const currentWindow: Window = { dataExplorerPlatform: 0 } as any;
|
|
||||||
|
|
||||||
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toEqual(currentWindow);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return current window's parent if current window's parent has dataExplorerPlatform property", () => {
|
|
||||||
const parentWindow: Window = { dataExplorerPlatform: 0 } as any;
|
|
||||||
const currentWindow: Window = { parent: parentWindow } as any;
|
|
||||||
|
|
||||||
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toEqual(parentWindow);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is reference to itself", () => {
|
|
||||||
const parentWindow: Window = {} as any;
|
|
||||||
(parentWindow as any).parent = parentWindow; // If a window does not have a parent, its parent property is a reference to itself.
|
|
||||||
const currentWindow: Window = { parent: parentWindow } as any;
|
|
||||||
|
|
||||||
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is not defined", () => {
|
|
||||||
const parentWindow: Window = {} as any;
|
|
||||||
const currentWindow: Window = { parent: parentWindow } as any;
|
|
||||||
|
|
||||||
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
|
|||||||
import Q from "q";
|
import Q from "q";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "./Constants";
|
import * as Constants from "./Constants";
|
||||||
|
import { getDataExplorerWindow } from "../Utils/WindowUtils";
|
||||||
|
|
||||||
export interface CachedDataPromise<T> {
|
export interface CachedDataPromise<T> {
|
||||||
deferred: Q.Deferred<T>;
|
deferred: Q.Deferred<T>;
|
||||||
@@ -48,38 +49,18 @@ export function sendCachedDataMessage<TResponseDataModel>(
|
|||||||
|
|
||||||
export function sendMessage(data: any): void {
|
export function sendMessage(data: any): void {
|
||||||
if (canSendMessage()) {
|
if (canSendMessage()) {
|
||||||
const dataExplorerWindow = getDataExplorerWindow(window);
|
// We try to find data explorer window first, then fallback to current window
|
||||||
if (dataExplorerWindow) {
|
const portalChildWindow = getDataExplorerWindow(window) || window;
|
||||||
dataExplorerWindow.parent.postMessage(
|
portalChildWindow.parent.postMessage(
|
||||||
{
|
{
|
||||||
signature: "pcIframe",
|
signature: "pcIframe",
|
||||||
data: data
|
data: data
|
||||||
},
|
},
|
||||||
dataExplorerWindow.document.referrer
|
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 {
|
export function canSendMessage(): boolean {
|
||||||
return window.parent !== window;
|
return window.parent !== window;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
|
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||||
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
|
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
|
||||||
@@ -237,19 +236,19 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns a production endpoint", () => {
|
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");
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a development endpoint", () => {
|
it("returns a development endpoint", () => {
|
||||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
|
const endpoint = getEndpoint();
|
||||||
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a guest endpoint", () => {
|
it("returns a guest endpoint", () => {
|
||||||
window.authType = AuthType.EncryptedToken;
|
window.authType = AuthType.EncryptedToken;
|
||||||
const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
|
const endpoint = getEndpoint();
|
||||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
|||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import EnvironmentUtility from "./EnvironmentUtility";
|
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
@@ -78,7 +77,7 @@ export function queryDocuments(
|
|||||||
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
|
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(databaseAccount) || "";
|
const endpoint = getEndpoint() || "";
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
@@ -139,7 +138,7 @@ export function readDocument(
|
|||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(databaseAccount);
|
const endpoint = getEndpoint();
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -179,7 +178,7 @@ export function createDocument(
|
|||||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
|
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(databaseAccount);
|
const endpoint = getEndpoint();
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||||
@@ -221,7 +220,7 @@ export function updateDocument(
|
|||||||
pk:
|
pk:
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(databaseAccount);
|
const endpoint = getEndpoint();
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
@@ -260,7 +259,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
|||||||
pk:
|
pk:
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(databaseAccount);
|
const endpoint = getEndpoint();
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
@@ -303,7 +302,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
autoPilotThroughput: params.autoPilotMaxThroughput?.toString()
|
autoPilotThroughput: params.autoPilotMaxThroughput?.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(databaseAccount);
|
const endpoint = getEndpoint();
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(
|
.fetch(
|
||||||
@@ -327,12 +326,9 @@ export function createMongoCollectionWithProxy(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEndpoint(databaseAccount: DataModels.DatabaseAccount): string {
|
export function getEndpoint(): string {
|
||||||
const serverId = window.dataExplorer.serverId();
|
|
||||||
const extensionEndpoint = window.dataExplorer.extensionEndpoint();
|
const extensionEndpoint = window.dataExplorer.extensionEndpoint();
|
||||||
let url = configContext.MONGO_BACKEND_ENDPOINT
|
let url = (configContext.MONGO_BACKEND_ENDPOINT || extensionEndpoint) + "/api/mongo/explorer";
|
||||||
? configContext.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer"
|
|
||||||
: EnvironmentUtility.getMongoBackendEndpoint(serverId, databaseAccount.location, extensionEndpoint);
|
|
||||||
|
|
||||||
if (window.authType === AuthType.EncryptedToken) {
|
if (window.authType === AuthType.EncryptedToken) {
|
||||||
url = url.replace("api/mongo", "api/guest/mongo");
|
url = url.replace("api/mongo", "api/guest/mongo");
|
||||||
|
|||||||
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;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ export enum Platform {
|
|||||||
|
|
||||||
interface ConfigContext {
|
interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
allowedParentFrameOrigins: RegExp;
|
allowedParentFrameOrigins: string[];
|
||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
proxyPath?: string;
|
proxyPath?: string;
|
||||||
AAD_ENDPOINT: string;
|
AAD_ENDPOINT: string;
|
||||||
@@ -30,7 +30,12 @@ interface ConfigContext {
|
|||||||
// Default configuration
|
// Default configuration
|
||||||
let configContext: Readonly<ConfigContext> = {
|
let configContext: Readonly<ConfigContext> = {
|
||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
allowedParentFrameOrigins: /^https:\/\/portal\.azure\.com$|^https:\/\/portal\.azure\.us$|^https:\/\/portal\.azure\.cn$|^https:\/\/portal\.microsoftazure\.de$|^https:\/\/.+\.portal\.azure\.com$|^https:\/\/.+\.portal\.azure\.us$|^https:\/\/.+\.portal\.azure\.cn$|^https:\/\/.+\.portal\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.com$|^https:\/\/main\.documentdb\.ext\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.cn$|^https:\/\/main\.documentdb\.ext\.azure\.us$/,
|
allowedParentFrameOrigins: [
|
||||||
|
`^https:\\/\\/cosmos.azure.(com|cn|us)$`,
|
||||||
|
`^https:\\/\\/[\\.\\w]+.portal.azure.(com|cn|us)$`,
|
||||||
|
`^https:\\/\\/[\\.\\w]+.ext.azure.(com|cn|us)$`,
|
||||||
|
`^https:\\/\\/[\\.\\w]+microsoftazure.de$`
|
||||||
|
],
|
||||||
// Webpack injects this at build time
|
// Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
@@ -73,8 +78,13 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
|||||||
const response = await fetch("./config.json");
|
const response = await fetch("./config.json");
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
try {
|
try {
|
||||||
const externalConfig = await response.json();
|
const { allowedParentFrameOrigins, ...externalConfig } = await response.json();
|
||||||
Object.assign(configContext, externalConfig);
|
Object.assign(configContext, externalConfig);
|
||||||
|
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
|
||||||
|
updateConfigContext({
|
||||||
|
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins]
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Unable to parse json in config file");
|
console.error("Unable to parse json in config file");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -289,6 +289,12 @@ export interface CreateCollectionParams {
|
|||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReadDatabaseOfferParams {
|
||||||
|
databaseId: string;
|
||||||
|
databaseResourceId?: string;
|
||||||
|
offerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: string;
|
id: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
|
|||||||
@@ -81,15 +81,15 @@ export interface Database extends TreeNode {
|
|||||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||||
|
|
||||||
selectDatabase(): void;
|
selectDatabase(): void;
|
||||||
expandDatabase(): void;
|
expandDatabase(): Promise<void>;
|
||||||
collapseDatabase(): void;
|
collapseDatabase(): void;
|
||||||
|
|
||||||
loadCollections(): Q.Promise<void>;
|
loadCollections(): Promise<void>;
|
||||||
findCollectionWithId(collectionRid: string): Collection;
|
findCollectionWithId(collectionRid: string): Collection;
|
||||||
openAddCollection(database: Database, event: MouseEvent): void;
|
openAddCollection(database: Database, event: MouseEvent): void;
|
||||||
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
|
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
|
||||||
readSettings(): void;
|
|
||||||
onSettingsClick: () => void;
|
onSettingsClick: () => void;
|
||||||
|
loadOffer(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionBase extends TreeNode {
|
export interface CollectionBase extends TreeNode {
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export class ResourceTreeContextMenuButtonFactory {
|
|||||||
const deleteDatabaseMenuItem = {
|
const deleteDatabaseMenuItem = {
|
||||||
iconSrc: DeleteDatabaseIcon,
|
iconSrc: DeleteDatabaseIcon,
|
||||||
onClick: () => container.deleteDatabaseConfirmationPane.open(),
|
onClick: () => container.deleteDatabaseConfirmationPane.open(),
|
||||||
label: container.deleteDatabaseText()
|
label: container.deleteDatabaseText(),
|
||||||
|
styleClass: "deleteDatabaseMenuItem"
|
||||||
};
|
};
|
||||||
return [newCollectionMenuItem, deleteDatabaseMenuItem];
|
return [newCollectionMenuItem, deleteDatabaseMenuItem];
|
||||||
}
|
}
|
||||||
@@ -112,7 +113,8 @@ export class ResourceTreeContextMenuButtonFactory {
|
|||||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
|
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
|
||||||
},
|
},
|
||||||
label: container.deleteCollectionText()
|
label: container.deleteCollectionText(),
|
||||||
|
styleClass: "deleteCollectionMenuItem"
|
||||||
});
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel {
|
|||||||
public onRemoveItemKeyPress = (data: any, event: KeyboardEvent, source: any): boolean => {
|
public onRemoveItemKeyPress = (data: any, event: KeyboardEvent, source: any): boolean => {
|
||||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||||
this.removeItem(data, event);
|
this.removeItem(data, event);
|
||||||
|
(document.querySelector(".dynamicListItem:last-of-type input") as HTMLElement).focus();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -94,7 +95,7 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel {
|
|||||||
|
|
||||||
public addItem(): void {
|
public addItem(): void {
|
||||||
this.listItems.push({ value: ko.observable("") });
|
this.listItems.push({ value: ko.observable("") });
|
||||||
document.getElementById("uniqueKeyItems").focus();
|
(document.querySelector(".dynamicListItem:last-of-type input") as HTMLElement).focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onAddItemKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
public onAddItemKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import * as Logger from "../../../Common/Logger";
|
|||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { StringUtils } from "../../../Utils/StringUtils";
|
import { StringUtils } from "../../../Utils/StringUtils";
|
||||||
|
import { userContext } from "../../../UserContext";
|
||||||
|
import { TerminalQueryParams } from "../../../Common/Constants";
|
||||||
|
|
||||||
export interface NotebookTerminalComponentProps {
|
export interface NotebookTerminalComponentProps {
|
||||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||||
@@ -32,11 +34,11 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
|||||||
|
|
||||||
public getTerminalParams(): Map<string, string> {
|
public getTerminalParams(): Map<string, string> {
|
||||||
let params: Map<string, string> = new Map<string, string>();
|
let params: Map<string, string> = new Map<string, string>();
|
||||||
params.set("terminal", "true");
|
params.set(TerminalQueryParams.Terminal, "true");
|
||||||
|
|
||||||
const terminalEndpoint: string = this.tryGetTerminalEndpoint();
|
const terminalEndpoint: string = this.tryGetTerminalEndpoint();
|
||||||
if (terminalEndpoint) {
|
if (terminalEndpoint) {
|
||||||
params.set("terminalEndpoint", terminalEndpoint);
|
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
@@ -75,11 +77,13 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
params.set("server", serverInfo.notebookServerEndpoint);
|
params.set(TerminalQueryParams.Server, serverInfo.notebookServerEndpoint);
|
||||||
if (serverInfo.authToken && serverInfo.authToken.length > 0) {
|
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?";
|
let result: string = "terminal.html?";
|
||||||
for (let key of params.keys()) {
|
for (let key of params.keys()) {
|
||||||
result += `${key}=${encodeURIComponent(params.get(key))}&`;
|
result += `${key}=${encodeURIComponent(params.get(key))}&`;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface NotebookViewerComponentProps {
|
|||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
backNavigationText: string;
|
backNavigationText: string;
|
||||||
hideInputs?: boolean;
|
hideInputs?: boolean;
|
||||||
|
hidePrompts?: boolean;
|
||||||
onBackClick: () => void;
|
onBackClick: () => void;
|
||||||
onTagClick: (tag: string) => void;
|
onTagClick: (tag: string) => void;
|
||||||
}
|
}
|
||||||
@@ -148,7 +149,8 @@ export class NotebookViewerComponent extends React.Component<
|
|||||||
{this.state.showProgressBar && <ProgressIndicator />}
|
{this.state.showProgressBar && <ProgressIndicator />}
|
||||||
|
|
||||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
|
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
|
||||||
hideInputs: this.props.hideInputs
|
hideInputs: this.props.hideInputs,
|
||||||
|
hidePrompts: this.props.hidePrompts
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}
|
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}
|
||||||
|
|||||||
@@ -159,4 +159,20 @@ describe("TreeNodeComponent", () => {
|
|||||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders loading icon", () => {
|
||||||
|
const node: TreeNode = {
|
||||||
|
label: "label",
|
||||||
|
children: [],
|
||||||
|
isExpanded: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
node,
|
||||||
|
generation: 2,
|
||||||
|
paddingLeft: 9
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ import {
|
|||||||
|
|
||||||
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||||
|
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||||
|
|
||||||
export interface TreeNodeMenuItem {
|
export interface TreeNodeMenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
iconSrc?: string;
|
iconSrc?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
styleClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TreeNode {
|
export interface TreeNode {
|
||||||
@@ -37,6 +39,7 @@ export interface TreeNode {
|
|||||||
data?: any; // Piece of data corresponding to this node
|
data?: any; // Piece of data corresponding to this node
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
|
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
|
||||||
|
isLoading?: boolean;
|
||||||
isSelected?: () => boolean;
|
isSelected?: () => boolean;
|
||||||
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
|
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
|
||||||
onExpanded?: () => void;
|
onExpanded?: () => void;
|
||||||
@@ -183,6 +186,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
)}
|
)}
|
||||||
{node.contextMenu && this.renderContextMenuButton(node)}
|
{node.contextMenu && this.renderContextMenuButton(node)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="loadingIconContainer">
|
||||||
|
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
|
||||||
|
</div>
|
||||||
{node.children && (
|
{node.children && (
|
||||||
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
|
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
|
||||||
<div className="nodeChildren" data-test={node.label}>
|
<div className="nodeChildren" data-test={node.label}>
|
||||||
@@ -256,13 +262,20 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
|
onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
|
||||||
>
|
>
|
||||||
{props.item.onRenderIcon()}
|
{props.item.onRenderIcon()}
|
||||||
<span className="treeComponentMenuItemLabel">{props.item.text}</span>
|
<span
|
||||||
|
className={
|
||||||
|
"treeComponentMenuItemLabel" + (props.item.className ? ` ${props.item.className}Label` : "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.item.text}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
|
items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
|
||||||
key: menuItem.label,
|
key: menuItem.label,
|
||||||
text: menuItem.label,
|
text: menuItem.label,
|
||||||
disabled: menuItem.isDisabled,
|
disabled: menuItem.isDisabled,
|
||||||
|
className: menuItem.styleClass,
|
||||||
onClick: menuItem.onClick,
|
onClick: menuItem.onClick,
|
||||||
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
|||||||
label
|
label
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="loadingIconContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="loadingIcon"
|
||||||
|
hidden={true}
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
animateOpacity={false}
|
animateOpacity={false}
|
||||||
animationStateClasses={
|
animationStateClasses={
|
||||||
@@ -179,6 +188,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||||||
"isBeakVisible": false,
|
"isBeakVisible": false,
|
||||||
"items": Array [
|
"items": Array [
|
||||||
Object {
|
Object {
|
||||||
|
"className": undefined,
|
||||||
"disabled": true,
|
"disabled": true,
|
||||||
"key": "menuLabel",
|
"key": "menuLabel",
|
||||||
"onClick": undefined,
|
"onClick": undefined,
|
||||||
@@ -201,6 +211,15 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="loadingIconContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="loadingIcon"
|
||||||
|
hidden={true}
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
animateOpacity={false}
|
animateOpacity={false}
|
||||||
animationStateClasses={
|
animationStateClasses={
|
||||||
@@ -261,6 +280,77 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||||
|
<div
|
||||||
|
className=" main2 nodeItem "
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="treeNodeHeader "
|
||||||
|
data-test="label"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"paddingLeft": 9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="label branch is expanded"
|
||||||
|
className="expandCollapseIcon"
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
role="button"
|
||||||
|
src=""
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="nodeLabel"
|
||||||
|
title="label"
|
||||||
|
>
|
||||||
|
label
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="loadingIconContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="loadingIcon"
|
||||||
|
hidden={true}
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AnimateHeight
|
||||||
|
animateOpacity={false}
|
||||||
|
animationStateClasses={
|
||||||
|
Object {
|
||||||
|
"animating": "rah-animating",
|
||||||
|
"animatingDown": "rah-animating--down",
|
||||||
|
"animatingToHeightAuto": "rah-animating--to-height-auto",
|
||||||
|
"animatingToHeightSpecific": "rah-animating--to-height-specific",
|
||||||
|
"animatingToHeightZero": "rah-animating--to-height-zero",
|
||||||
|
"animatingUp": "rah-animating--up",
|
||||||
|
"static": "rah-static",
|
||||||
|
"staticHeightAuto": "rah-static--height-auto",
|
||||||
|
"staticHeightSpecific": "rah-static--height-specific",
|
||||||
|
"staticHeightZero": "rah-static--height-zero",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyInlineTransitions={true}
|
||||||
|
delay={0}
|
||||||
|
duration={200}
|
||||||
|
easing="ease"
|
||||||
|
height="auto"
|
||||||
|
style={Object {}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="nodeChildren"
|
||||||
|
data-test="label"
|
||||||
|
/>
|
||||||
|
</AnimateHeight>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
||||||
<div
|
<div
|
||||||
className="nodeClassname main12 nodeItem "
|
className="nodeClassname main12 nodeItem "
|
||||||
@@ -331,6 +421,15 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="loadingIconContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="loadingIcon"
|
||||||
|
hidden={true}
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
animateOpacity={false}
|
animateOpacity={false}
|
||||||
animationStateClasses={
|
animationStateClasses={
|
||||||
@@ -450,6 +549,15 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
|
|||||||
label
|
label
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="loadingIconContainer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="loadingIcon"
|
||||||
|
hidden={true}
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
animateOpacity={false}
|
animateOpacity={false}
|
||||||
animationStateClasses={
|
animationStateClasses={
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.showingMenu {
|
&.showingMenu {
|
||||||
background-color: #EEE;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.treeMenuEllipsis {
|
.treeMenuEllipsis {
|
||||||
@@ -78,3 +78,12 @@
|
|||||||
vertical-align: text-bottom;
|
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 Database from "./Tree/Database";
|
||||||
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
||||||
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
|
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
|
||||||
import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
import { refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
||||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||||
@@ -1424,71 +1424,40 @@ export default class Explorer {
|
|||||||
|
|
||||||
// TODO: Refactor
|
// TODO: Refactor
|
||||||
const deferred: Q.Deferred<any> = Q.defer();
|
const deferred: Q.Deferred<any> = Q.defer();
|
||||||
|
this._setLoadingStatusText("Fetching databases...");
|
||||||
const refreshDatabases = (offers?: DataModels.Offer[]) => {
|
readDatabases().then(
|
||||||
this._setLoadingStatusText("Fetching databases...");
|
(databases: DataModels.Database[]) => {
|
||||||
readDatabases().then(
|
this._setLoadingStatusText("Successfully fetched databases.");
|
||||||
(databases: DataModels.Database[]) => {
|
TelemetryProcessor.traceSuccess(
|
||||||
this._setLoadingStatusText("Successfully fetched databases.");
|
Action.LoadDatabases,
|
||||||
TelemetryProcessor.traceSuccess(
|
{
|
||||||
Action.LoadDatabases,
|
databaseAccountName: this.databaseAccount().name,
|
||||||
{
|
defaultExperience: this.defaultExperience(),
|
||||||
databaseAccountName: this.databaseAccount().name,
|
dataExplorerArea: Constants.Areas.ResourceTree
|
||||||
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
|
reason => {
|
||||||
);
|
this._setLoadingStatusText("Failed to fetch containers.");
|
||||||
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
|
deferred.reject(reason);
|
||||||
const deltaDatabases = this.getDeltaDatabases(databases, offers);
|
}
|
||||||
this.addDatabasesToList(deltaDatabases.toAdd);
|
)
|
||||||
this.deleteDatabasesFromList(deltaDatabases.toDelete);
|
.finally(() => this.isRefreshingExplorer(false));
|
||||||
this.selectedNode(currentlySelectedNode);
|
|
||||||
this._setLoadingStatusText("Fetching containers...");
|
|
||||||
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
this._setLoadingStatusText("Successfully fetched containers.");
|
|
||||||
deferred.resolve();
|
|
||||||
},
|
|
||||||
reason => {
|
|
||||||
this._setLoadingStatusText("Failed to fetch containers.");
|
|
||||||
deferred.reject(reason);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => this.isRefreshingExplorer(false));
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
this._setLoadingStatusText("Failed to fetch databases.");
|
|
||||||
this.isRefreshingExplorer(false);
|
|
||||||
deferred.reject(error);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.LoadDatabases,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.databaseAccount().name,
|
|
||||||
defaultExperience: this.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
|
||||||
error: JSON.stringify(error)
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() });
|
|
||||||
this._setLoadingStatusText("Fetching offers...");
|
|
||||||
offerPromise.then(
|
|
||||||
(offers: DataModels.Offer[]) => {
|
|
||||||
this._setLoadingStatusText("Successfully fetched offers.");
|
|
||||||
refreshDatabases(offers);
|
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this._setLoadingStatusText("Failed to fetch offers.");
|
this._setLoadingStatusText("Failed to fetch databases.");
|
||||||
this.isRefreshingExplorer(false);
|
this.isRefreshingExplorer(false);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
@@ -2103,16 +2072,13 @@ export default class Explorer {
|
|||||||
defaultExperience: this.defaultExperience && this.defaultExperience(),
|
defaultExperience: this.defaultExperience && this.defaultExperience(),
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree
|
dataExplorerArea: Constants.Areas.ResourceTree
|
||||||
});
|
});
|
||||||
databasesToLoad.forEach((database: ViewModels.Database) => {
|
databasesToLoad.forEach(async (database: ViewModels.Database) => {
|
||||||
loadCollectionPromises.push(
|
await database.loadCollections();
|
||||||
database.loadCollections().finally(() => {
|
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
|
||||||
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
|
if (isNewDatabase) {
|
||||||
if (isNewDatabase) {
|
database.expandDatabase();
|
||||||
database.expandDatabase();
|
}
|
||||||
}
|
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
|
||||||
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Q.all(loadCollectionPromises).done(
|
Q.all(loadCollectionPromises).done(
|
||||||
@@ -2257,8 +2223,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getDeltaDatabases(
|
private getDeltaDatabases(
|
||||||
updatedDatabaseList: DataModels.Database[],
|
updatedDatabaseList: DataModels.Database[]
|
||||||
updatedOffersList: DataModels.Offer[]
|
|
||||||
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } {
|
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } {
|
||||||
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
|
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
|
||||||
const databaseExists = _.some(
|
const databaseExists = _.some(
|
||||||
@@ -2267,10 +2232,9 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
return !databaseExists;
|
return !databaseExists;
|
||||||
});
|
});
|
||||||
const databasesToAdd: ViewModels.Database[] = _.map(newDatabases, (newDatabase: DataModels.Database) => {
|
const databasesToAdd: ViewModels.Database[] = newDatabases.map(
|
||||||
const databaseOffer: DataModels.Offer = this.getOfferForResource(updatedOffersList, newDatabase._self);
|
(newDatabase: DataModels.Database) => new Database(this, newDatabase)
|
||||||
return new Database(this, newDatabase, databaseOffer);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
let databasesToDelete: ViewModels.Database[] = [];
|
let databasesToDelete: ViewModels.Database[] = [];
|
||||||
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
|
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
|
||||||
@@ -2320,10 +2284,6 @@ export default class Explorer {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOfferForResource(offers: DataModels.Offer[], resourceId: string): DataModels.Offer {
|
|
||||||
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||||
@@ -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;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createScaleAndSettingsButton(container: Explorer): CommandButtonComponentProps {
|
|
||||||
let isShared = false;
|
|
||||||
if (container.isDatabaseNodeSelected()) {
|
|
||||||
isShared = container.findSelectedDatabase().isDatabaseShared();
|
|
||||||
} else if (container.isNodeKindSelected("Collection")) {
|
|
||||||
const database: ViewModels.Database = container.findSelectedCollection().getDatabase();
|
|
||||||
isShared = database && database.isDatabaseShared();
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = isShared ? "Settings" : "Scale & Settings";
|
|
||||||
|
|
||||||
return {
|
|
||||||
iconSrc: ScaleIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: () => {
|
|
||||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
|
||||||
selectedCollection && (<any>selectedCollection).onSettingsClick();
|
|
||||||
},
|
|
||||||
commandButtonLabel: label,
|
|
||||||
ariaLabel: label,
|
|
||||||
hasPopup: true,
|
|
||||||
disabled: container.isDatabaseNodeOrNoneSelected()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||||
const label = "New Notebook";
|
const label = "New Notebook";
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -113,11 +113,14 @@ export default class NotebookManager {
|
|||||||
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
|
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
|
||||||
this.params.resourceTree.triggerRender();
|
this.params.resourceTree.triggerRender();
|
||||||
});
|
});
|
||||||
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
this.refreshPinnedRepos();
|
||||||
}
|
}
|
||||||
|
|
||||||
public refreshPinnedRepos(): void {
|
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(
|
public async openPublishNotebookPane(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import "./base.css";
|
|||||||
import "./default.css";
|
import "./default.css";
|
||||||
|
|
||||||
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
|
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
|
||||||
|
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
|
||||||
import { AzureTheme } from "./AzureTheme";
|
import { AzureTheme } from "./AzureTheme";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -15,6 +16,7 @@ import "./NotebookReadOnlyRenderer.less";
|
|||||||
export interface NotebookRendererProps {
|
export interface NotebookRendererProps {
|
||||||
contentRef: any;
|
contentRef: any;
|
||||||
hideInputs?: boolean;
|
hideInputs?: boolean;
|
||||||
|
hidePrompts?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PassedEditorProps {
|
interface PassedEditorProps {
|
||||||
@@ -38,6 +40,29 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
loadTransform(this.props as any);
|
loadTransform(this.props as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderPrompt(id: string, contentRef: string): JSX.Element {
|
||||||
|
if (this.props.hidePrompts) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Prompt id={id} contentRef={contentRef}>
|
||||||
|
{(props: PassedPromptProps) => {
|
||||||
|
if (props.status === "busy") {
|
||||||
|
return <React.Fragment>{"[*]"}</React.Fragment>;
|
||||||
|
}
|
||||||
|
if (props.status === "queued") {
|
||||||
|
return <React.Fragment>{"[…]"}</React.Fragment>;
|
||||||
|
}
|
||||||
|
if (typeof props.executionCount === "number") {
|
||||||
|
return <React.Fragment>{`[${props.executionCount}]`}</React.Fragment>;
|
||||||
|
}
|
||||||
|
return <React.Fragment>{"[ ]"}</React.Fragment>;
|
||||||
|
}}
|
||||||
|
</Prompt>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="NotebookReadOnlyRender">
|
<div className="NotebookReadOnlyRender">
|
||||||
@@ -46,6 +71,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
||||||
<CodeCell id={id} contentRef={contentRef}>
|
<CodeCell id={id} contentRef={contentRef}>
|
||||||
{{
|
{{
|
||||||
|
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
|
||||||
editor: {
|
editor: {
|
||||||
codemirror: (props: PassedEditorProps) =>
|
codemirror: (props: PassedEditorProps) =>
|
||||||
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />
|
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />
|
||||||
|
|||||||
@@ -115,10 +115,10 @@
|
|||||||
|
|
||||||
<!-- Database provisioned throughput - Start -->
|
<!-- Database provisioned throughput - Start -->
|
||||||
<!-- ko if: canConfigureThroughput -->
|
<!-- ko if: canConfigureThroughput -->
|
||||||
<div class="databaseProvision" aria-label="New database provision support"
|
<div class="databaseProvision" aria-label="Provision database throughput"
|
||||||
data-bind="visible: databaseCreateNew">
|
data-bind="visible: databaseCreateNew">
|
||||||
<input tabindex="0" type="checkbox" data-test="addCollectionPane-databaseSharedThroughput"
|
<input tabindex="0" type="checkbox" data-test="addCollectionPane-databaseSharedThroughput"
|
||||||
id="addCollection-databaseSharedThroughput" title="Provision shared throughput"
|
id="addCollection-databaseSharedThroughput" title="Provision database throughput"
|
||||||
data-bind="checked: databaseCreateNewShared" />
|
data-bind="checked: databaseCreateNewShared" />
|
||||||
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span>
|
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span>
|
||||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||||
@@ -517,13 +517,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<span class="mandatoryStar">*</span>
|
<span class="mandatoryStar">*</span>
|
||||||
<span class="addCollectionLabel">Analytical store</span>
|
<span class="addCollectionLabel">Analytical store</span>
|
||||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
<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">
|
<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
|
Enable analytical store capability to perform near real-time analytics on your operational
|
||||||
data, without impacting the performance of transactional workloads.
|
data, without impacting the performance of transactional workloads.
|
||||||
Learn more <a class="errorLink" href="https://aka.ms/analytical-store-overview"
|
Learn more <a id="link1" class="errorLink" href="https://aka.ms/analytical-store-overview"
|
||||||
target="_blank">here</a>
|
target="_blank" data-bind="event: { focusout: onFocusOut, keydown: onKeyDown.bind($data, 'largePartitionKey') }">here</a>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -608,7 +608,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.container.isPreferredApiMongoDB() && this.container.hasStorageAnalyticsAfecFeature()) {
|
if (this.container.isPreferredApiMongoDB()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,7 +666,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
const subscriptionType: ViewModels.SubscriptionType =
|
const subscriptionType: ViewModels.SubscriptionType =
|
||||||
this.container.subscriptionType && this.container.subscriptionType();
|
this.container.subscriptionType && this.container.subscriptionType();
|
||||||
|
|
||||||
if (subscriptionType === ViewModels.SubscriptionType.EA) {
|
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,7 +681,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
public open(databaseId?: string) {
|
public async open(databaseId?: string) {
|
||||||
super.open();
|
super.open();
|
||||||
// TODO: Figure out if a database level partition split is about to happen once shared throughput read is available
|
// TODO: Figure out if a database level partition split is about to happen once shared throughput read is available
|
||||||
this.formWarnings("");
|
this.formWarnings("");
|
||||||
@@ -715,18 +715,40 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
dataExplorerArea: Constants.Areas.ContextualPane
|
dataExplorerArea: Constants.Areas.ContextualPane
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await this.container.loadDatabaseOffers();
|
||||||
this._onDatabasesChange(this.container.databases());
|
this._onDatabasesChange(this.container.databases());
|
||||||
this._setFocus();
|
this._setFocus();
|
||||||
|
|
||||||
TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage);
|
TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private transferFocus(elementIdToKeepVisible: string, elementIdToFocus: string): void {
|
||||||
|
document.getElementById(elementIdToKeepVisible).style.visibility = "visible";
|
||||||
|
document.getElementById(elementIdToFocus).focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFocusOut(_: any, event: any): void {
|
||||||
|
event.target.parentElement.style.visibility = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMouseOut(_: any, event: any): void {
|
||||||
|
event.target.style.visibility = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyDown(previousActiveElementId: string, _: any, event: KeyboardEvent): boolean {
|
||||||
|
if (event.shiftKey && event.keyCode == Constants.KeyCodes.Tab) {
|
||||||
|
document.getElementById(previousActiveElementId).focus();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// Execute default action
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) {
|
private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) {
|
||||||
const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => {
|
const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => {
|
||||||
if (database && database.offer && database.offer()) {
|
if (database && database.offer && database.offer()) {
|
||||||
this._databaseOffers.set(database.id(), database.offer());
|
this._databaseOffers.set(database.id(), database.offer());
|
||||||
} else if (database && database.isDatabaseShared && database.isDatabaseShared()) {
|
|
||||||
database.readSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return database.id();
|
return database.id();
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
const subscriptionType: ViewModels.SubscriptionType =
|
const subscriptionType: ViewModels.SubscriptionType =
|
||||||
this.container.subscriptionType && this.container.subscriptionType();
|
this.container.subscriptionType && this.container.subscriptionType();
|
||||||
|
|
||||||
if (subscriptionType === ViewModels.SubscriptionType.EA) {
|
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -268,8 +268,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
|
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
|
||||||
if (keyspace && keyspace.offer && !!keyspace.offer()) {
|
if (keyspace && keyspace.offer && !!keyspace.offer()) {
|
||||||
this.keyspaceOffers.set(keyspace.id(), keyspace.offer());
|
this.keyspaceOffers.set(keyspace.id(), keyspace.offer());
|
||||||
} else if (keyspace && keyspace.isDatabaseShared && keyspace.isDatabaseShared()) {
|
|
||||||
keyspace.readSettings();
|
|
||||||
}
|
}
|
||||||
return keyspace.id();
|
return keyspace.id();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Explorer from "../Explorer";
|
|||||||
|
|
||||||
// TODO: Use specific actions for logging telemetry data
|
// TODO: Use specific actions for logging telemetry data
|
||||||
export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
|
export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
|
||||||
|
private initalFocusedElement: HTMLElement | undefined;
|
||||||
public id: string;
|
public id: string;
|
||||||
public container: Explorer;
|
public container: Explorer;
|
||||||
public firstFieldHasFocus: ko.Observable<boolean>;
|
public firstFieldHasFocus: ko.Observable<boolean>;
|
||||||
@@ -49,9 +50,11 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
|
|||||||
this.visible(false);
|
this.visible(false);
|
||||||
this.isExecuting(false);
|
this.isExecuting(false);
|
||||||
this.resetData();
|
this.resetData();
|
||||||
|
this.resetFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public open() {
|
public open() {
|
||||||
|
this.initalFocusedElement = document.activeElement as HTMLElement;
|
||||||
this.visible(true);
|
this.visible(true);
|
||||||
this.firstFieldHasFocus(true);
|
this.firstFieldHasFocus(true);
|
||||||
this.resizePane();
|
this.resizePane();
|
||||||
@@ -123,4 +126,11 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
|
|||||||
|
|
||||||
$(paneElement).height(newPaneElementHeight);
|
$(paneElement).height(newPaneElementHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resetFocus(): void {
|
||||||
|
if (this.initalFocusedElement) {
|
||||||
|
this.initalFocusedElement.focus();
|
||||||
|
this.initalFocusedElement = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,8 +67,7 @@
|
|||||||
name="collectionIdConfirmation"
|
name="collectionIdConfirmation"
|
||||||
required
|
required
|
||||||
class="collid"
|
class="collid"
|
||||||
data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus"
|
data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus, attr: { 'aria-label': collectionIdConfirmationText }"
|
||||||
aria-label="Confirm by typing the collection id"
|
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -134,11 +134,9 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||||||
expect(telemetryProcessorSpy.called).toBe(true);
|
expect(telemetryProcessorSpy.called).toBe(true);
|
||||||
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
|
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
|
||||||
expect(
|
expect(
|
||||||
telemetryProcessorSpy.calledWith(
|
telemetryProcessorSpy.calledWith(Action.DeleteCollection, ActionModifiers.Mark, {
|
||||||
Action.DeleteCollection,
|
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
|
||||||
ActionModifiers.Mark,
|
})
|
||||||
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
|
|
||||||
)
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,11 +88,9 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
|
|||||||
this.containerDeleteFeedback()
|
this.containerDeleteFeedback()
|
||||||
);
|
);
|
||||||
|
|
||||||
TelemetryProcessor.trace(
|
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
|
||||||
Action.DeleteCollection,
|
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
|
||||||
ActionModifiers.Mark,
|
});
|
||||||
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
|
|
||||||
);
|
|
||||||
|
|
||||||
this.containerDeleteFeedback("");
|
this.containerDeleteFeedback("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,11 +120,9 @@ describe("Delete Database Confirmation Pane", () => {
|
|||||||
|
|
||||||
return pane.submit().then(() => {
|
return pane.submit().then(() => {
|
||||||
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
|
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
|
||||||
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(
|
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteDatabase, ActionModifiers.Mark, {
|
||||||
Action.DeleteDatabase,
|
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
|
||||||
ActionModifiers.Mark,
|
});
|
||||||
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -97,11 +97,9 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
|||||||
this.databaseDeleteFeedback()
|
this.databaseDeleteFeedback()
|
||||||
);
|
);
|
||||||
|
|
||||||
TelemetryProcessor.trace(
|
TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, {
|
||||||
Action.DeleteDatabase,
|
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
|
||||||
ActionModifiers.Mark,
|
});
|
||||||
JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback))
|
|
||||||
);
|
|
||||||
|
|
||||||
this.databaseDeleteFeedback("");
|
this.databaseDeleteFeedback("");
|
||||||
}
|
}
|
||||||
@@ -132,7 +130,8 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
|||||||
super.resetData();
|
super.resetData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public open() {
|
public async open() {
|
||||||
|
await this.container.loadSelectedDatabaseOffer();
|
||||||
this.recordDeleteFeedback(this.shouldRecordFeedback());
|
this.recordDeleteFeedback(this.shouldRecordFeedback());
|
||||||
super.open();
|
super.open();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export default class AddTableEntityPane extends TableEntityPane {
|
|||||||
);
|
);
|
||||||
this.updateIsActionEnabled();
|
this.updateIsActionEnabled();
|
||||||
super.open();
|
super.open();
|
||||||
|
this.focusValueElement();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.displayedAttributes(
|
this.displayedAttributes(
|
||||||
@@ -79,7 +80,11 @@ export default class AddTableEntityPane extends TableEntityPane {
|
|||||||
);
|
);
|
||||||
this.updateIsActionEnabled();
|
this.updateIsActionEnabled();
|
||||||
super.open();
|
super.open();
|
||||||
|
this.focusValueElement();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private focusValueElement() {
|
||||||
const focusElement = document.getElementById("addTableEntityValue");
|
const focusElement = document.getElementById("addTableEntityValue");
|
||||||
focusElement && focusElement.focus();
|
focusElement && focusElement.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<div class="firstdivbg headerline">
|
<div class="firstdivbg headerline">
|
||||||
<span role="heading" aria-level="2" data-bind="text: title"></span>
|
<span role="heading" aria-level="2" data-bind="text: title"></span>
|
||||||
<div
|
<div
|
||||||
|
id="closeAddEntityPane"
|
||||||
class="closeImg"
|
class="closeImg"
|
||||||
role="button"
|
role="button"
|
||||||
aria-label="Close pane"
|
aria-label="Close pane"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { AuthType } from "../../AuthType";
|
|||||||
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as Entities from "./Entities";
|
import * as Entities from "./Entities";
|
||||||
import EnvironmentUtility from "../../Common/EnvironmentUtility";
|
|
||||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
@@ -308,7 +307,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
authType === AuthType.EncryptedToken
|
authType === AuthType.EncryptedToken
|
||||||
? Constants.CassandraBackend.guestQueryApi
|
? Constants.CassandraBackend.guestQueryApi
|
||||||
: Constants.CassandraBackend.queryApi;
|
: Constants.CassandraBackend.queryApi;
|
||||||
$.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`, {
|
$.ajax(`${collection.container.extensionEndpoint()}${apiEndpoint}`, {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: {
|
data: {
|
||||||
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
||||||
@@ -559,7 +558,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
authType === AuthType.EncryptedToken
|
authType === AuthType.EncryptedToken
|
||||||
? Constants.CassandraBackend.guestKeysApi
|
? Constants.CassandraBackend.guestKeysApi
|
||||||
: Constants.CassandraBackend.keysApi;
|
: Constants.CassandraBackend.keysApi;
|
||||||
let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`;
|
let endpoint = `${collection.container.extensionEndpoint()}${apiEndpoint}`;
|
||||||
const deferred = Q.defer<CassandraTableKeys>();
|
const deferred = Q.defer<CassandraTableKeys>();
|
||||||
$.ajax(endpoint, {
|
$.ajax(endpoint, {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
@@ -614,7 +613,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
authType === AuthType.EncryptedToken
|
authType === AuthType.EncryptedToken
|
||||||
? Constants.CassandraBackend.guestSchemaApi
|
? Constants.CassandraBackend.guestSchemaApi
|
||||||
: Constants.CassandraBackend.schemaApi;
|
: Constants.CassandraBackend.schemaApi;
|
||||||
let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`;
|
let endpoint = `${collection.container.extensionEndpoint()}${apiEndpoint}`;
|
||||||
const deferred = Q.defer<CassandraTableKey[]>();
|
const deferred = Q.defer<CassandraTableKey[]>();
|
||||||
$.ajax(endpoint, {
|
$.ajax(endpoint, {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
@@ -668,7 +667,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
authType === AuthType.EncryptedToken
|
authType === AuthType.EncryptedToken
|
||||||
? Constants.CassandraBackend.guestCreateOrDeleteApi
|
? Constants.CassandraBackend.guestCreateOrDeleteApi
|
||||||
: Constants.CassandraBackend.createOrDeleteApi;
|
: Constants.CassandraBackend.createOrDeleteApi;
|
||||||
$.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(explorer)}${apiEndpoint}`, {
|
$.ajax(`${explorer.extensionEndpoint()}${apiEndpoint}`, {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: {
|
data: {
|
||||||
accountName: explorer.databaseAccount() && explorer.databaseAccount().name,
|
accountName: explorer.databaseAccount() && explorer.databaseAccount().name,
|
||||||
|
|||||||
@@ -598,7 +598,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
() => {
|
() => {
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this._setBaseline();
|
this._setBaseline();
|
||||||
this.database.readSettings();
|
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.UpdateSettings,
|
Action.UpdateSettings,
|
||||||
{
|
{
|
||||||
@@ -643,8 +642,9 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onActivate(): Q.Promise<any> {
|
public onActivate(): Q.Promise<any> {
|
||||||
return super.onActivate().then(() => {
|
return super.onActivate().then(async () => {
|
||||||
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
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 ko from "knockout";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import AuthHeadersUtil from "../../Platform/Hosted/Authorization";
|
import AuthHeadersUtil from "../../Platform/Hosted/Authorization";
|
||||||
import EnvironmentUtility from "../../Common/EnvironmentUtility";
|
|
||||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
@@ -109,11 +108,7 @@ export default class MongoShellTab extends TabsBase {
|
|||||||
) + Constants.MongoDBAccounts.defaultPort.toString();
|
) + Constants.MongoDBAccounts.defaultPort.toString();
|
||||||
const databaseId = this.collection.databaseId;
|
const databaseId = this.collection.databaseId;
|
||||||
const collectionId = this.collection.id();
|
const collectionId = this.collection.id();
|
||||||
const apiEndpoint = EnvironmentUtility.getMongoBackendEndpoint(
|
const apiEndpoint = this._container.extensionEndpoint();
|
||||||
this._container.serverId(),
|
|
||||||
userContext.databaseAccount.location,
|
|
||||||
this._container.extensionEndpoint()
|
|
||||||
).replace("/api/mongo/explorer", "");
|
|
||||||
const encryptedAuthToken: string = userContext.accessToken;
|
const encryptedAuthToken: string = userContext.accessToken;
|
||||||
|
|
||||||
shellIframe.contentWindow.postMessage(
|
shellIframe.contentWindow.postMessage(
|
||||||
@@ -142,7 +137,7 @@ export default class MongoShellTab extends TabsBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataToLog: string = event.data.data.logData;
|
const dataToLog = { message: event.data.data.logData };
|
||||||
const logType: string = event.data.data.logType;
|
const logType: string = event.data.data.logType;
|
||||||
const shellTraceId: string = event.data.data.traceId || "none";
|
const shellTraceId: string = event.data.data.traceId || "none";
|
||||||
|
|
||||||
|
|||||||
@@ -346,7 +346,6 @@ describe("Settings tab", () => {
|
|||||||
|
|
||||||
const offer: DataModels.Offer = null;
|
const offer: DataModels.Offer = null;
|
||||||
const defaultTtl = 200;
|
const defaultTtl = 200;
|
||||||
const database = new Database(explorer, baseDatabase, null);
|
|
||||||
const conflictResolutionPolicy = {
|
const conflictResolutionPolicy = {
|
||||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||||
conflictResolutionPath: "/_ts"
|
conflictResolutionPath: "/_ts"
|
||||||
@@ -507,7 +506,6 @@ describe("Settings tab", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const database = new Database(explorer, baseDatabase, null);
|
|
||||||
const container: DataModels.Collection = {
|
const container: DataModels.Collection = {
|
||||||
_rid: "_rid",
|
_rid: "_rid",
|
||||||
_self: "",
|
_self: "",
|
||||||
|
|||||||
@@ -1270,8 +1270,10 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onActivate(): Q.Promise<any> {
|
public onActivate(): Q.Promise<any> {
|
||||||
return super.onActivate().then(() => {
|
return super.onActivate().then(async () => {
|
||||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
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 { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { readOffers, readOffer } from "../../Common/DocumentClientUtilityBase";
|
|
||||||
import { readCollections } from "../../Common/dataAccess/readCollections";
|
import { readCollections } from "../../Common/dataAccess/readCollections";
|
||||||
|
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||||
|
|
||||||
export default class Database implements ViewModels.Database {
|
export default class Database implements ViewModels.Database {
|
||||||
public nodeKind: string;
|
public nodeKind: string;
|
||||||
@@ -27,13 +27,13 @@ export default class Database implements ViewModels.Database {
|
|||||||
public isDatabaseShared: ko.Computed<boolean>;
|
public isDatabaseShared: ko.Computed<boolean>;
|
||||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||||
|
|
||||||
constructor(container: Explorer, data: any, offer: DataModels.Offer) {
|
constructor(container: Explorer, data: any) {
|
||||||
this.nodeKind = "Database";
|
this.nodeKind = "Database";
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.self = data._self;
|
this.self = data._self;
|
||||||
this.rid = data._rid;
|
this.rid = data._rid;
|
||||||
this.id = ko.observable(data.id);
|
this.id = ko.observable(data.id);
|
||||||
this.offer = ko.observable(offer);
|
this.offer = ko.observable();
|
||||||
this.collections = ko.observableArray<Collection>();
|
this.collections = ko.observableArray<Collection>();
|
||||||
this.isDatabaseExpanded = ko.observable<boolean>(false);
|
this.isDatabaseExpanded = ko.observable<boolean>(false);
|
||||||
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
||||||
@@ -66,7 +66,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
dataExplorerArea: Constants.Areas.Tab,
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
tabTitle: "Scale"
|
tabTitle: "Scale"
|
||||||
});
|
});
|
||||||
Q.all([pendingNotificationsPromise, this.readSettings()]).then(
|
pendingNotificationsPromise.then(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
const pendingNotification: DataModels.Notification = data && data[0];
|
const pendingNotification: DataModels.Notification = data && data[0];
|
||||||
settingsTab = new DatabaseSettingsTab({
|
settingsTab = new DatabaseSettingsTab({
|
||||||
@@ -121,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 {
|
public isDatabaseNodeSelected(): boolean {
|
||||||
return (
|
return (
|
||||||
!this.isDatabaseExpanded() &&
|
!this.isDatabaseExpanded() &&
|
||||||
@@ -219,23 +145,13 @@ export default class Database implements ViewModels.Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public expandCollapseDatabase() {
|
public async expandDatabase() {
|
||||||
this.selectDatabase();
|
|
||||||
if (this.isDatabaseExpanded()) {
|
|
||||||
this.collapseDatabase();
|
|
||||||
} else {
|
|
||||||
this.expandDatabase();
|
|
||||||
}
|
|
||||||
this.container.onUpdateTabsButtons([]);
|
|
||||||
this.container.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === this.rid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public expandDatabase() {
|
|
||||||
if (this.isDatabaseExpanded()) {
|
if (this.isDatabaseExpanded()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadCollections();
|
await this.loadOffer();
|
||||||
|
await this.loadCollections();
|
||||||
this.isDatabaseExpanded(true);
|
this.isDatabaseExpanded(true);
|
||||||
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
|
||||||
description: "Database node",
|
description: "Database node",
|
||||||
@@ -259,32 +175,19 @@ export default class Database implements ViewModels.Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadCollections(): Q.Promise<void> {
|
public async loadCollections(): Promise<void> {
|
||||||
let collectionVMs: Collection[] = [];
|
const collectionVMs: Collection[] = [];
|
||||||
let deferred: Q.Deferred<void> = Q.defer<void>();
|
const collections: DataModels.Collection[] = await readCollections(this.id());
|
||||||
|
const deltaCollections = this.getDeltaCollections(collections);
|
||||||
|
|
||||||
readCollections(this.id()).then(
|
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
||||||
(collections: DataModels.Collection[]) => {
|
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
|
||||||
let collectionsToAddVMPromises: Q.Promise<any>[] = [];
|
collectionVMs.push(collectionVM);
|
||||||
let deltaCollections = this.getDeltaCollections(collections);
|
});
|
||||||
|
|
||||||
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
//merge collections
|
||||||
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
|
this.addCollectionsToList(collectionVMs);
|
||||||
collectionVMs.push(collectionVM);
|
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
||||||
});
|
|
||||||
|
|
||||||
//merge collections
|
|
||||||
this.addCollectionsToList(collectionVMs);
|
|
||||||
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
|
||||||
|
|
||||||
deferred.resolve();
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public openAddCollection(database: Database, event: MouseEvent) {
|
public openAddCollection(database: Database, event: MouseEvent) {
|
||||||
@@ -296,6 +199,16 @@ export default class Database implements ViewModels.Database {
|
|||||||
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId);
|
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async loadOffer(): Promise<void> {
|
||||||
|
if (!this.container.isServerlessEnabled() && !this.offer()) {
|
||||||
|
const params: DataModels.ReadDatabaseOfferParams = {
|
||||||
|
databaseId: this.id(),
|
||||||
|
databaseResourceId: this.self
|
||||||
|
};
|
||||||
|
this.offer(await readDatabaseOffer(params));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
||||||
if (!this.container) {
|
if (!this.container) {
|
||||||
return Q.resolve(undefined);
|
return Q.resolve(undefined);
|
||||||
@@ -376,6 +289,10 @@ export default class Database implements ViewModels.Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private deleteCollectionsFromList(collectionsToRemove: Collection[]): void {
|
private deleteCollectionsFromList(collectionsToRemove: Collection[]): void {
|
||||||
|
if (collectionsToRemove.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const collectionsToKeep: Collection[] = [];
|
const collectionsToKeep: Collection[] = [];
|
||||||
|
|
||||||
ko.utils.arrayForEach(this.collections(), (collection: Collection) => {
|
ko.utils.arrayForEach(this.collections(), (collection: Collection) => {
|
||||||
@@ -387,8 +304,4 @@ export default class Database implements ViewModels.Database {
|
|||||||
|
|
||||||
this.collections(collectionsToKeep);
|
this.collections(collectionsToKeep);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getOfferForDatabase(offers: DataModels.Offer[], database: DataModels.Database): DataModels.Offer {
|
|
||||||
return _.find(offers, (offer: DataModels.Offer) => offer.resource === database._self);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
this.container.nonSystemDatabases.subscribe((databases: ViewModels.Database[]) => {
|
this.container.nonSystemDatabases.subscribe((databases: ViewModels.Database[]) => {
|
||||||
// Clean up old databases
|
// Clean up old databases
|
||||||
this.cleanupDatabasesKoSubs(databases.map((database: ViewModels.Database) => database.id()));
|
this.cleanupDatabasesKoSubs();
|
||||||
|
|
||||||
databases.forEach((database: ViewModels.Database) => this.watchDatabase(database));
|
databases.forEach((database: ViewModels.Database) => this.watchDatabase(database));
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
@@ -170,14 +170,17 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
children: [],
|
children: [],
|
||||||
isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined),
|
isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined),
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database),
|
||||||
onClick: isExpanded => {
|
onClick: async isExpanded => {
|
||||||
// Rewritten version of expandCollapseDatabase():
|
// Rewritten version of expandCollapseDatabase():
|
||||||
if (!isExpanded) {
|
if (isExpanded) {
|
||||||
database.expandDatabase();
|
|
||||||
database.loadCollections();
|
|
||||||
} else {
|
|
||||||
database.collapseDatabase();
|
database.collapseDatabase();
|
||||||
|
} else {
|
||||||
|
if (databaseNode.children?.length === 0) {
|
||||||
|
databaseNode.isLoading = true;
|
||||||
|
}
|
||||||
|
await database.expandDatabase();
|
||||||
}
|
}
|
||||||
|
databaseNode.isLoading = false;
|
||||||
database.selectDatabase();
|
database.selectDatabase();
|
||||||
this.container.onUpdateTabsButtons([]);
|
this.container.onUpdateTabsButtons([]);
|
||||||
this.container.tabsManager.refreshActiveTab(
|
this.container.tabsManager.refreshActiveTab(
|
||||||
@@ -203,6 +206,12 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
databaseNode.children.push(this.buildCollectionNode(database, collection))
|
databaseNode.children.push(this.buildCollectionNode(database, collection))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
database.collections.subscribe((collections: ViewModels.Collection[]) => {
|
||||||
|
collections.forEach((collection: ViewModels.Collection) =>
|
||||||
|
databaseNode.children.push(this.buildCollectionNode(database, collection))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return databaseNode;
|
return databaseNode;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -790,16 +799,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
this.koSubsCollectionIdMap.push(collectionId, sub);
|
this.koSubsCollectionIdMap.push(collectionId, sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanupDatabasesKoSubs(existingDatabaseIds: string[]): void {
|
private cleanupDatabasesKoSubs(): void {
|
||||||
const databaseIdsToRemove = this.databaseCollectionIdMap
|
this.koSubsDatabaseIdMap.keys().forEach((databaseId: string) => {
|
||||||
.keys()
|
this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose());
|
||||||
.filter((id: string) => existingDatabaseIds.indexOf(id) === -1);
|
this.koSubsDatabaseIdMap.delete(databaseId);
|
||||||
|
|
||||||
databaseIdsToRemove.forEach((databaseId: string) => {
|
|
||||||
if (this.koSubsDatabaseIdMap.has(databaseId)) {
|
|
||||||
this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose());
|
|
||||||
this.koSubsDatabaseIdMap.delete(databaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.databaseCollectionIdMap.has(databaseId)) {
|
if (this.databaseCollectionIdMap.has(databaseId)) {
|
||||||
this.databaseCollectionIdMap
|
this.databaseCollectionIdMap
|
||||||
|
|||||||
@@ -35,13 +35,19 @@ const onInit = async () => {
|
|||||||
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
|
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
|
||||||
galleryItem = galleryItemJunoResponse.data;
|
galleryItem = galleryItemJunoResponse.data;
|
||||||
}
|
}
|
||||||
render(notebookUrl, backNavigationText, hideInputs, galleryItem, onBackClick);
|
|
||||||
|
// The main purpose of hiding the prompt is to hide everything when hiding inputs.
|
||||||
|
// It is generally not very useful to just hide the prompt.
|
||||||
|
const hidePrompts = hideInputs;
|
||||||
|
|
||||||
|
render(notebookUrl, backNavigationText, hideInputs, hidePrompts, galleryItem, onBackClick);
|
||||||
};
|
};
|
||||||
|
|
||||||
const render = (
|
const render = (
|
||||||
notebookUrl: string,
|
notebookUrl: string,
|
||||||
backNavigationText: string,
|
backNavigationText: string,
|
||||||
hideInputs: boolean,
|
hideInputs?: boolean,
|
||||||
|
hidePrompts?: boolean,
|
||||||
galleryItem?: IGalleryItem,
|
galleryItem?: IGalleryItem,
|
||||||
onBackClick?: () => void
|
onBackClick?: () => void
|
||||||
) => {
|
) => {
|
||||||
@@ -51,6 +57,7 @@ const render = (
|
|||||||
galleryItem,
|
galleryItem,
|
||||||
backNavigationText,
|
backNavigationText,
|
||||||
hideInputs,
|
hideInputs,
|
||||||
|
hidePrompts,
|
||||||
onBackClick: onBackClick,
|
onBackClick: onBackClick,
|
||||||
onTagClick: undefined
|
onTagClick: undefined
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
|||||||
import { appInsights } from "../appInsights";
|
import { appInsights } from "../appInsights";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { getDataExplorerWindow } from "../../Utils/WindowUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that persists telemetry data to the portal tables.
|
* Class that persists telemetry data to the portal tables.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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({
|
sendMessage({
|
||||||
type: MessageTypes.TelemetryInfo,
|
type: MessageTypes.TelemetryInfo,
|
||||||
data: {
|
data: {
|
||||||
@@ -22,7 +25,7 @@ export function trace(action: Action, actionModifier: string = ActionModifiers.M
|
|||||||
appInsights.trackEvent({ name: Action[action] }, getData(actionModifier, data));
|
appInsights.trackEvent({ name: Action[action] }, getData(actionModifier, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function traceStart(action: Action, data?: unknown): number {
|
export function traceStart(action: Action, data?: TelemetryData): number {
|
||||||
const timestamp: number = Date.now();
|
const timestamp: number = Date.now();
|
||||||
sendMessage({
|
sendMessage({
|
||||||
type: MessageTypes.TelemetryInfo,
|
type: MessageTypes.TelemetryInfo,
|
||||||
@@ -38,7 +41,7 @@ export function traceStart(action: Action, data?: unknown): number {
|
|||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function traceSuccess(action: Action, data?: unknown, timestamp?: number): void {
|
export function traceSuccess(action: Action, data?: TelemetryData, timestamp?: number): void {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
type: MessageTypes.TelemetryInfo,
|
type: MessageTypes.TelemetryInfo,
|
||||||
data: {
|
data: {
|
||||||
@@ -52,7 +55,7 @@ export function traceSuccess(action: Action, data?: unknown, timestamp?: number)
|
|||||||
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Success, data));
|
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({
|
sendMessage({
|
||||||
type: MessageTypes.TelemetryInfo,
|
type: MessageTypes.TelemetryInfo,
|
||||||
data: {
|
data: {
|
||||||
@@ -66,7 +69,7 @@ export function traceFailure(action: Action, data?: unknown, timestamp?: number)
|
|||||||
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Failed, data));
|
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({
|
sendMessage({
|
||||||
type: MessageTypes.TelemetryInfo,
|
type: MessageTypes.TelemetryInfo,
|
||||||
data: {
|
data: {
|
||||||
@@ -80,7 +83,7 @@ export function traceCancel(action: Action, data?: unknown, timestamp?: number):
|
|||||||
appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Cancel, data));
|
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();
|
const validTimestamp = timestamp || Date.now();
|
||||||
sendMessage({
|
sendMessage({
|
||||||
type: MessageTypes.TelemetryInfo,
|
type: MessageTypes.TelemetryInfo,
|
||||||
@@ -96,7 +99,7 @@ export function traceOpen(action: Action, data?: unknown, timestamp?: number): n
|
|||||||
return validTimestamp;
|
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();
|
const validTimestamp = timestamp || Date.now();
|
||||||
sendMessage({
|
sendMessage({
|
||||||
type: MessageTypes.TelemetryInfo,
|
type: MessageTypes.TelemetryInfo,
|
||||||
@@ -112,21 +115,16 @@ export function traceMark(action: Action, data?: unknown, timestamp?: number): n
|
|||||||
return validTimestamp;
|
return validTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getData(actionModifier: string, data: unknown = {}): { [key: string]: string } | undefined {
|
function getData(actionModifier: string, data: TelemetryData = {}): { [key: string]: string } {
|
||||||
if (typeof data === "string") {
|
const dataExplorerWindow = getDataExplorerWindow(window);
|
||||||
data = { message: data };
|
return {
|
||||||
}
|
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
|
||||||
if (typeof data === "object") {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return {
|
authType: dataExplorerWindow && (dataExplorerWindow as any).authType,
|
||||||
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
|
subscriptionId: userContext.subscriptionId as string,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
platform: configContext.platform,
|
||||||
authType: (window as any).authType,
|
env: process.env.NODE_ENV as string,
|
||||||
subscriptionId: userContext.subscriptionId as string,
|
actionModifier,
|
||||||
platform: configContext.platform,
|
...data
|
||||||
env: process.env.NODE_ENV as string,
|
};
|
||||||
actionModifier,
|
|
||||||
...data
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { ServerConnection } from "@jupyterlab/services";
|
|||||||
import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
|
import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
|
||||||
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { updateUserContext } from "../UserContext";
|
||||||
|
import { TerminalQueryParams } from "../Common/Constants";
|
||||||
|
|
||||||
const getUrlVars = (): { [key: string]: string } => {
|
const getUrlVars = (): { [key: string]: string } => {
|
||||||
const vars: { [key: string]: string } = {};
|
const vars: { [key: string]: string } = {};
|
||||||
@@ -18,22 +20,22 @@ const getUrlVars = (): { [key: string]: string } => {
|
|||||||
|
|
||||||
const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => {
|
const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => {
|
||||||
let body: BodyInit;
|
let body: BodyInit;
|
||||||
if (urlVars.hasOwnProperty("terminalEndpoint")) {
|
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
|
||||||
body = JSON.stringify({
|
body = JSON.stringify({
|
||||||
endpoint: urlVars["terminalEndpoint"]
|
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = urlVars["server"];
|
const server = urlVars[TerminalQueryParams.Server];
|
||||||
let options: Partial<ServerConnection.ISettings> = {
|
let options: Partial<ServerConnection.ISettings> = {
|
||||||
baseUrl: server,
|
baseUrl: server,
|
||||||
init: { body },
|
init: { body },
|
||||||
fetch: window.parent.fetch
|
fetch: window.parent.fetch
|
||||||
};
|
};
|
||||||
if (urlVars.hasOwnProperty("token")) {
|
if (urlVars.hasOwnProperty(TerminalQueryParams.Token)) {
|
||||||
options = {
|
options = {
|
||||||
baseUrl: server,
|
baseUrl: server,
|
||||||
token: urlVars["token"],
|
token: urlVars[TerminalQueryParams.Token],
|
||||||
init: { body },
|
init: { body },
|
||||||
fetch: window.parent.fetch
|
fetch: window.parent.fetch
|
||||||
};
|
};
|
||||||
@@ -44,6 +46,12 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect
|
|||||||
|
|
||||||
const main = async (): Promise<void> => {
|
const main = async (): Promise<void> => {
|
||||||
const urlVars = getUrlVars();
|
const urlVars = getUrlVars();
|
||||||
|
|
||||||
|
// Initialize userContext. Currently only subscrptionId is required by TelemetryProcessor
|
||||||
|
updateUserContext({
|
||||||
|
subscriptionId: urlVars[TerminalQueryParams.SubscriptionId]
|
||||||
|
});
|
||||||
|
|
||||||
const serverSettings = createServerSettings(urlVars);
|
const serverSettings = createServerSettings(urlVars);
|
||||||
|
|
||||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, {
|
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, {
|
||||||
@@ -51,7 +59,7 @@ const main = async (): Promise<void> => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (urlVars.hasOwnProperty("terminal")) {
|
if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) {
|
||||||
await JupyterLabAppFactory.createTerminalApp(serverSettings);
|
await JupyterLabAppFactory.createTerminalApp(serverSettings);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Only terminal is supported");
|
throw new Error("Only terminal is supported");
|
||||||
|
|||||||
21
src/Utils/MessageValidation.test.ts
Normal file
21
src/Utils/MessageValidation.test.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { isInvalidParentFrameOrigin } from "./MessageValidation";
|
||||||
|
|
||||||
|
test.each`
|
||||||
|
domain | expected
|
||||||
|
${"https://cosmos.azure.com"} | ${false}
|
||||||
|
${"https://cosmos.azure.us"} | ${false}
|
||||||
|
${"https://cosmos.azure.cn"} | ${false}
|
||||||
|
${"https://cosmos.microsoftazure.de"} | ${false}
|
||||||
|
${"https://subdomain.portal.azure.com"} | ${false}
|
||||||
|
${"https://subdomain.portal.azure.us"} | ${false}
|
||||||
|
${"https://subdomain.portal.azure.cn"} | ${false}
|
||||||
|
${"https://subdomain.microsoftazure.de"} | ${false}
|
||||||
|
${"https://main.documentdb.ext.azure.com"} | ${false}
|
||||||
|
${"https://main.documentdb.ext.azure.us"} | ${false}
|
||||||
|
${"https://main.documentdb.ext.azure.cn"} | ${false}
|
||||||
|
${"https://main.documentdb.ext.microsoftazure.de"} | ${false}
|
||||||
|
${"https://random.domain"} | ${true}
|
||||||
|
${"https://malicious.cloudapp.azure.com"} | ${true}
|
||||||
|
`("returns $expected when called with $domain", ({ domain, expected }) => {
|
||||||
|
expect(isInvalidParentFrameOrigin({ origin: domain } as MessageEvent)).toBe(expected);
|
||||||
|
});
|
||||||
@@ -4,13 +4,18 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
|
|||||||
return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
|
return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidOrigin(allowedOrigins: RegExp, event: MessageEvent): boolean {
|
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
|
||||||
const eventOrigin = (event && event.origin) || "";
|
const eventOrigin = (event && event.origin) || "";
|
||||||
const windowOrigin = (window && window.origin) || "";
|
const windowOrigin = (window && window.origin) || "";
|
||||||
if (eventOrigin === windowOrigin) {
|
if (eventOrigin === windowOrigin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = allowedOrigins && allowedOrigins.test(eventOrigin);
|
for (const origin of allowedOrigins) {
|
||||||
return result;
|
const result = new RegExp(origin).test(eventOrigin);
|
||||||
|
if (result) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
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 "expect-puppeteer";
|
||||||
import crypto from 'crypto'
|
import crypto from "crypto";
|
||||||
|
|
||||||
jest.setTimeout(300000);
|
jest.setTimeout(300000);
|
||||||
|
|
||||||
describe('Collection Add and Delete SQL spec', () => {
|
describe("Collection Add and Delete SQL spec", () => {
|
||||||
it('creates a collection', async () => {
|
it("creates a collection", async () => {
|
||||||
try {
|
try {
|
||||||
const dbId = `TestDatabase${crypto.randomBytes(8).toString("hex")}`;
|
const dbId = `TestDatabase${crypto.randomBytes(8).toString("hex")}`;
|
||||||
const collectionId = `TestCollection${crypto.randomBytes(8).toString("hex")}`;
|
const collectionId = `TestCollection${crypto.randomBytes(8).toString("hex")}`;
|
||||||
@@ -13,10 +13,10 @@ describe('Collection Add and Delete SQL spec', () => {
|
|||||||
page.goto(prodUrl);
|
page.goto(prodUrl);
|
||||||
|
|
||||||
// log in with connection string
|
// log in with connection string
|
||||||
const handle = await page.waitForSelector('iframe');
|
const handle = await page.waitForSelector("iframe");
|
||||||
const frame = await handle.contentFrame();
|
const frame = await handle.contentFrame();
|
||||||
await frame.waitFor('div > p.switchConnectTypeText', { visible: true });
|
await frame.waitFor("div > p.switchConnectTypeText", { visible: true });
|
||||||
await frame.click('div > p.switchConnectTypeText');
|
await frame.click("div > p.switchConnectTypeText");
|
||||||
const connStr = process.env.PORTAL_RUNNER_CONNECTION_STRING;
|
const connStr = process.env.PORTAL_RUNNER_CONNECTION_STRING;
|
||||||
await frame.type("input[class='inputToken']", connStr);
|
await frame.type("input[class='inputToken']", connStr);
|
||||||
await frame.click("input[value='Connect']");
|
await frame.click("input[value='Connect']");
|
||||||
@@ -32,7 +32,7 @@ describe('Collection Add and Delete SQL spec', () => {
|
|||||||
|
|
||||||
// check shared throughput
|
// check shared throughput
|
||||||
await frame.waitFor('input[data-test="addCollectionPane-databaseSharedThroughput"]');
|
await frame.waitFor('input[data-test="addCollectionPane-databaseSharedThroughput"]');
|
||||||
await frame.click('input[data-test="addCollectionPane-databaseSharedThroughput"]') ;
|
await frame.click('input[data-test="addCollectionPane-databaseSharedThroughput"]');
|
||||||
|
|
||||||
// type database id
|
// type database id
|
||||||
await frame.waitFor('input[data-test="addCollection-newDatabaseId"]');
|
await frame.waitFor('input[data-test="addCollection-newDatabaseId"]');
|
||||||
@@ -47,8 +47,8 @@ describe('Collection Add and Delete SQL spec', () => {
|
|||||||
await frame.type('input[data-test="addCollection-partitionKeyValue"]', sharedKey);
|
await frame.type('input[data-test="addCollection-partitionKeyValue"]', sharedKey);
|
||||||
|
|
||||||
// click submit
|
// click submit
|
||||||
await frame.waitFor('#submitBtnAddCollection');
|
await frame.waitFor("#submitBtnAddCollection");
|
||||||
await frame.click('#submitBtnAddCollection');
|
await frame.click("#submitBtnAddCollection");
|
||||||
|
|
||||||
// validate created
|
// validate created
|
||||||
// open database menu
|
// open database menu
|
||||||
@@ -56,24 +56,27 @@ describe('Collection Add and Delete SQL spec', () => {
|
|||||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||||
|
|
||||||
await frame.click(`div[data-test="${dbId}"]`);
|
await frame.click(`div[data-test="${dbId}"]`);
|
||||||
await frame.waitFor(`span[title="${collectionId}"]`);
|
await frame.waitFor(3000);
|
||||||
|
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
|
||||||
|
|
||||||
// delete container
|
// delete container
|
||||||
|
|
||||||
// click context menu for container
|
// click context menu for container
|
||||||
await frame.waitFor(`div[data-test="${collectionId}"] > div > button`);
|
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.click(`div[data-test="${collectionId}"] > div > button`);
|
||||||
|
await frame.waitFor(2000);
|
||||||
|
|
||||||
// click delete container
|
// click delete container
|
||||||
await frame.waitForSelector('body > div.ms-Layer.ms-Layer--fixed');
|
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]', { visible: true });
|
||||||
await frame.waitFor(1000);
|
await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
|
||||||
const elements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
|
|
||||||
await elements[4].click();
|
|
||||||
|
|
||||||
// confirm delete container
|
// confirm delete container
|
||||||
|
await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true });
|
||||||
await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim());
|
await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim());
|
||||||
|
|
||||||
// click delete
|
// click delete
|
||||||
|
await frame.waitFor('input[data-test="deleteCollection"]', { visible: true });
|
||||||
await frame.click('input[data-test="deleteCollection"]');
|
await frame.click('input[data-test="deleteCollection"]');
|
||||||
await frame.waitFor(5000);
|
await frame.waitFor(5000);
|
||||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||||
@@ -87,9 +90,8 @@ describe('Collection Add and Delete SQL spec', () => {
|
|||||||
await button.asElement().click();
|
await button.asElement().click();
|
||||||
|
|
||||||
// click delete database
|
// click delete database
|
||||||
await frame.waitFor(1000);
|
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
|
||||||
const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
|
await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
|
||||||
await dbElements[1].click();
|
|
||||||
|
|
||||||
// confirm delete database
|
// confirm delete database
|
||||||
await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim());
|
await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim());
|
||||||
@@ -99,8 +101,10 @@ describe('Collection Add and Delete SQL spec', () => {
|
|||||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||||
await expect(page).not.toMatchElement(`div[data-test="${dbId}"]`);
|
await expect(page).not.toMatchElement(`div[data-test="${dbId}"]`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
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;
|
throw error;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"sourceMap": false,
|
"sourceMap": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const childProcess = require("child_process");
|
|||||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const isCI = require("is-ci");
|
const isCI = require("is-ci");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
|
||||||
const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8");
|
const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8");
|
||||||
|
|
||||||
@@ -104,6 +105,15 @@ module.exports = function(env = {}, argv = {}) {
|
|||||||
envVars.NODE_ENV = "development";
|
envVars.NODE_ENV = "development";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sourceMapPlugin =
|
||||||
|
mode === "development"
|
||||||
|
? new webpack.EvalSourceMapDevToolPlugin({})
|
||||||
|
: new webpack.SourceMapDevToolPlugin({
|
||||||
|
// test: [".js", ".mjs", ".css", ".ts", ".tsx"],
|
||||||
|
filename: "[name].js.map",
|
||||||
|
exclude: [/vendor/]
|
||||||
|
});
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
new CleanWebpackPlugin(["dist"]),
|
new CleanWebpackPlugin(["dist"]),
|
||||||
new CreateFileWebpack({
|
new CreateFileWebpack({
|
||||||
@@ -164,7 +174,9 @@ module.exports = function(env = {}, argv = {}) {
|
|||||||
new CopyWebpackPlugin({
|
new CopyWebpackPlugin({
|
||||||
patterns: [{ from: "DataExplorer.nuspec" }, { from: "web.config" }, { from: "quickstart/*.zip" }]
|
patterns: [{ from: "DataExplorer.nuspec" }, { from: "web.config" }, { from: "quickstart/*.zip" }]
|
||||||
}),
|
}),
|
||||||
new EnvironmentPlugin(envVars)
|
new EnvironmentPlugin(envVars),
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
|
||||||
|
sourceMapPlugin
|
||||||
];
|
];
|
||||||
|
|
||||||
if (argv.analyze) {
|
if (argv.analyze) {
|
||||||
@@ -194,7 +206,6 @@ module.exports = function(env = {}, argv = {}) {
|
|||||||
filename: "[name].[chunkhash:6].js",
|
filename: "[name].[chunkhash:6].js",
|
||||||
path: path.resolve(__dirname, "dist")
|
path: path.resolve(__dirname, "dist")
|
||||||
},
|
},
|
||||||
devtool: mode === "development" ? "cheap-eval-source-map" : "source-map",
|
|
||||||
plugins,
|
plugins,
|
||||||
module: {
|
module: {
|
||||||
rules
|
rules
|
||||||
@@ -206,6 +217,7 @@ module.exports = function(env = {}, argv = {}) {
|
|||||||
minimize: mode === "production" ? true : false,
|
minimize: mode === "production" ? true : false,
|
||||||
minimizer: [
|
minimizer: [
|
||||||
new TerserPlugin({
|
new TerserPlugin({
|
||||||
|
sourceMap: true,
|
||||||
cache: ".cache/terser",
|
cache: ".cache/terser",
|
||||||
terserOptions: {
|
terserOptions: {
|
||||||
// These options increase our initial bundle size by ~5% but the builds are significantly faster and won't run out of memory
|
// These options increase our initial bundle size by ~5% but the builds are significantly faster and won't run out of memory
|
||||||
|
|||||||
Reference in New Issue
Block a user