mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-26 12:21:23 +00:00
Compare commits
11 Commits
users/aisa
...
fix_a11y_D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c25d132954 | ||
|
|
15eedb898d | ||
|
|
b7f0548cca | ||
|
|
4728dc48d7 | ||
|
|
9358fd5889 | ||
|
|
f5da8bb276 | ||
|
|
de5df90f75 | ||
|
|
66421ad276 | ||
|
|
e70fa01a8b | ||
|
|
79b6f3cf2f | ||
|
|
b765cae088 |
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -12,7 +12,8 @@
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"--runInBand",
|
||||
"--coverage", "false"
|
||||
"--coverage",
|
||||
"false"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
@@ -26,7 +27,8 @@
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"${fileBasenameNoExtension}",
|
||||
"--coverage", "false",
|
||||
"--coverage",
|
||||
"false",
|
||||
// "--watch",
|
||||
// // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints.
|
||||
// // https://github.com/facebook/jest/issues/6683
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
||||
import { CosmosHeaders } from "@azure/cosmos/dist-esm";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@@ -77,10 +78,21 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
|
||||
}
|
||||
}
|
||||
|
||||
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
||||
enum SDKSupportedCapabilities {
|
||||
None = 0,
|
||||
PartitionMerge = 1 << 0,
|
||||
}
|
||||
|
||||
let _client: Cosmos.CosmosClient;
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) return _client;
|
||||
|
||||
let _defaultHeaders: CosmosHeaders = {};
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supported-capabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
|
||||
const options: Cosmos.CosmosClientOptions = {
|
||||
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||
key: userContext.masterKey,
|
||||
@@ -89,6 +101,7 @@ export function client(): Cosmos.CosmosClient {
|
||||
enableEndpointDiscovery: false,
|
||||
},
|
||||
userAgentSuffix: "Azure Portal",
|
||||
defaultHeaders: _defaultHeaders,
|
||||
};
|
||||
|
||||
if (configContext.PROXY_PATH !== undefined) {
|
||||
|
||||
@@ -236,13 +236,12 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
});
|
||||
|
||||
it("returns a development endpoint", () => {
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint("https://localhost:1234");
|
||||
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
||||
});
|
||||
|
||||
@@ -250,7 +249,7 @@ describe("MongoProxyClient", () => {
|
||||
updateUserContext({
|
||||
authType: AuthType.EncryptedToken,
|
||||
});
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import queryString from "querystring";
|
||||
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
@@ -336,14 +337,17 @@ export function createMongoCollectionWithProxy(
|
||||
}
|
||||
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
return hasFlag(userContext.features.mongoProxyAPIs, feature)
|
||||
? getEndpoint(userContext.features.mongoProxyEndpoint)
|
||||
: getEndpoint();
|
||||
const endpoint =
|
||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||
? userContext.features.mongoProxyEndpoint
|
||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
|
||||
return getEndpoint(endpoint);
|
||||
}
|
||||
|
||||
export function getEndpoint(customEndpoint?: string): string {
|
||||
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
url += "/api/mongo/explorer";
|
||||
export function getEndpoint(endpoint: string): string {
|
||||
let url = endpoint + "/api/mongo/explorer";
|
||||
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { JunoEndpoints } from "Common/Constants";
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedArmEndpoints,
|
||||
allowedBackendEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointValidation";
|
||||
|
||||
export enum Platform {
|
||||
Portal = "Portal",
|
||||
@@ -8,7 +20,7 @@ export enum Platform {
|
||||
|
||||
export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedParentFrameOrigins: string[];
|
||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
AAD_ENDPOINT: string;
|
||||
@@ -30,7 +42,6 @@ export interface ConfigContext {
|
||||
isTerminalEnabled: boolean;
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
allowedJunoOrigins: string[];
|
||||
msalRedirectURI?: string;
|
||||
}
|
||||
|
||||
@@ -44,8 +55,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`,
|
||||
],
|
||||
// Webpack injects this at build time
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||
@@ -61,14 +71,6 @@ let configContext: Readonly<ConfigContext> = {
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
isTerminalEnabled: false,
|
||||
allowedJunoOrigins: [
|
||||
JunoEndpoints.Test,
|
||||
JunoEndpoints.Test2,
|
||||
JunoEndpoints.Test3,
|
||||
JunoEndpoints.Prod,
|
||||
JunoEndpoints.Stage,
|
||||
"https://localhost",
|
||||
],
|
||||
};
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
@@ -79,6 +81,50 @@ export function resetConfigContext(): void {
|
||||
}
|
||||
|
||||
export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
if (!newContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) {
|
||||
delete newContext.ARM_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
|
||||
delete newContext.AAD_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
||||
delete newContext.EMULATOR_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
|
||||
delete newContext.GRAPH_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.ARCADIA_ENDPOINT, allowedArcadiaEndpoints)) {
|
||||
delete newContext.ARCADIA_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
|
||||
delete newContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
||||
delete newContext.JUNO_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.hostedExplorerURL, allowedHostedExplorerEndpoints)) {
|
||||
delete newContext.hostedExplorerURL;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.msalRedirectURI, allowedMsalRedirectEndpoints)) {
|
||||
delete newContext.msalRedirectURI;
|
||||
}
|
||||
|
||||
Object.assign(configContext, newContext);
|
||||
}
|
||||
|
||||
@@ -102,18 +148,8 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
});
|
||||
if (response.status === 200) {
|
||||
try {
|
||||
const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json();
|
||||
Object.assign(configContext, externalConfig);
|
||||
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
|
||||
});
|
||||
}
|
||||
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
|
||||
});
|
||||
}
|
||||
const { ...externalConfig } = await response.json();
|
||||
updateConfigContext(externalConfig);
|
||||
} catch (error) {
|
||||
console.error("Unable to parse json in config file");
|
||||
console.error(error);
|
||||
@@ -125,6 +161,14 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
const armAPIVersion = params.get("armAPIVersion") || "";
|
||||
updateConfigContext({ armAPIVersion });
|
||||
}
|
||||
if (params.has("armEndpoint")) {
|
||||
const ARM_ENDPOINT = params.get("armEndpoint") || "";
|
||||
updateConfigContext({ ARM_ENDPOINT });
|
||||
}
|
||||
if (params.has("aadEndpoint")) {
|
||||
const AAD_ENDPOINT = params.get("aadEndpoint") || "";
|
||||
updateConfigContext({ AAD_ENDPOINT });
|
||||
}
|
||||
if (params.has("platform")) {
|
||||
const platform = params.get("platform");
|
||||
switch (platform) {
|
||||
@@ -144,4 +188,3 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
}
|
||||
|
||||
export { configContext };
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export enum MessageTypes {
|
||||
CreateWorkspace,
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount,
|
||||
CloseTab,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
@@ -164,7 +164,6 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
<TextField
|
||||
multiline={useTextarea}
|
||||
rows={1}
|
||||
id="input"
|
||||
defaultValue={defaultValue}
|
||||
ariaLabel="Input query"
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -14,7 +14,6 @@ exports[`inputTypeahead renders <input /> 1`] = `
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
id="input"
|
||||
multiline={false}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
@@ -41,7 +40,6 @@ exports[`inputTypeahead renders <textarea /> 1`] = `
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
id="input"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
|
||||
@@ -55,6 +55,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testAccount,
|
||||
notebookServerInfo: testNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -65,6 +66,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo32Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -75,6 +77,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo36Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -85,6 +88,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testCassandraAccount,
|
||||
notebookServerInfo: testCassandraNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as StringUtils from "../../../Utils/StringUtils";
|
||||
export interface NotebookTerminalComponentProps {
|
||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
||||
@@ -55,6 +56,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
apiType: userContext.apiType,
|
||||
authType: userContext.authType,
|
||||
databaseAccount: userContext.databaseAccount,
|
||||
tabId: this.props.tabId,
|
||||
};
|
||||
|
||||
postRobot.send(this.terminalWindow, "props", props, {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Link } from "@fluentui/react/lib/Link";
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import _ from "underscore";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import shallow from "zustand/shallow";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
@@ -24,7 +26,6 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -32,11 +33,7 @@ import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import {
|
||||
get as getWorkspace,
|
||||
listByDatabaseAccount,
|
||||
start,
|
||||
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
@@ -50,13 +47,12 @@ import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||
import type NotebookManager from "./Notebook/NotebookManager";
|
||||
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||
@@ -178,7 +174,11 @@ export default class Explorer {
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
|
||||
// Override notebook server parameters from URL parameters
|
||||
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
|
||||
if (
|
||||
userContext.features.notebookServerUrl &&
|
||||
validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerToken
|
||||
) {
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken,
|
||||
@@ -190,19 +190,6 @@ export default class Explorer {
|
||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||
}
|
||||
|
||||
if (userContext.features.livyEndpoint) {
|
||||
useNotebook.getState().setSparkClusterConnectionInfo({
|
||||
userName: undefined,
|
||||
password: undefined,
|
||||
endpoints: [
|
||||
{
|
||||
endpoint: userContext.features.livyEndpoint,
|
||||
kind: DataModels.SparkClusterEndpointKind.Livy,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
@@ -422,7 +409,10 @@ export default class Explorer {
|
||||
connectionStatus.status = ConnectionStatusType.Connected;
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
|
||||
notebookServerEndpoint:
|
||||
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerUrl) ||
|
||||
connectionInfo.data.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||
forwardingId: connectionInfo.data.forwardingId,
|
||||
});
|
||||
@@ -472,35 +462,6 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureNotebookWorkspaceRunning() {
|
||||
if (!userContext.databaseAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
let clearMessage;
|
||||
try {
|
||||
const notebookWorkspace = await getWorkspace(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
if (
|
||||
notebookWorkspace &&
|
||||
notebookWorkspace.properties &&
|
||||
notebookWorkspace.properties.status &&
|
||||
notebookWorkspace.properties.status.toLowerCase() === "stopped"
|
||||
) {
|
||||
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
|
||||
await start(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
|
||||
} finally {
|
||||
clearMessage && clearMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private _resetNotebookWorkspace = async () => {
|
||||
useDialog.getState().closeDialog();
|
||||
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
||||
@@ -1096,7 +1057,7 @@ export default class Explorer {
|
||||
|
||||
const terminalTabs: TerminalTab[] = useTabs
|
||||
.getState()
|
||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[];
|
||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle().startsWith(title)) as TerminalTab[];
|
||||
|
||||
let index = 1;
|
||||
if (terminalTabs.length > 0) {
|
||||
@@ -1187,20 +1148,12 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
private _openSetupNotebooksPaneForQuickstart(): void {
|
||||
const title = "Enable Notebooks (Preview)";
|
||||
const description =
|
||||
"You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
|
||||
}
|
||||
|
||||
public async handleOpenFileAction(path: string): Promise<void> {
|
||||
if (useNotebook.getState().isPhoenixNotebooks === undefined) {
|
||||
await useNotebook.getState().getPhoenixStatus();
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.allocateContainer();
|
||||
} else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
|
||||
this._openSetupNotebooksPaneForQuickstart();
|
||||
}
|
||||
|
||||
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
|
||||
|
||||
@@ -31,28 +31,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Account is not serverless - button should be visible", () => {
|
||||
it("Button should be visible", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Account is serverless - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableServerless" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Enable notebook button", () => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
|
||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||
@@ -25,7 +24,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -36,7 +34,6 @@ import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPa
|
||||
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||
import { SetupNoteBooksPanel } from "../../Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { SelectedNodeState } from "../../useSelectedNode";
|
||||
|
||||
@@ -112,11 +109,6 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
} else {
|
||||
if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenixNotebooks) {
|
||||
buttons.push(createDivider());
|
||||
buttons.push(createEnableNotebooksButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||
@@ -274,10 +266,6 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isServerlessAccount()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -471,33 +459,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||
};
|
||||
}
|
||||
|
||||
function createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return undefined;
|
||||
}
|
||||
const label = "Enable Notebooks (Preview)";
|
||||
const tooltip =
|
||||
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const description =
|
||||
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
||||
return {
|
||||
iconSrc: EnableNotebooksIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
label,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
|
||||
),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
|
||||
ariaLabel: label,
|
||||
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Terminal";
|
||||
return {
|
||||
@@ -515,9 +476,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
const label = "Open Mongo Shell";
|
||||
const tooltip =
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const title = "Set up workspace";
|
||||
const description =
|
||||
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
@@ -526,13 +484,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -547,9 +498,6 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
const label = "Open Cassandra Shell";
|
||||
const tooltip =
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const title = "Set up workspace";
|
||||
const description =
|
||||
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
@@ -558,13 +506,6 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
} else {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
|
||||
@@ -667,7 +667,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Checkbox
|
||||
label="My partition key is larger than 100 bytes"
|
||||
label="My partition key is larger than 101 bytes"
|
||||
checked={this.state.useHashV2}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
@@ -887,10 +887,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isServerlessAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "SQL":
|
||||
case "Mongo":
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import { SetupNoteBooksPanel } from "./SetupNotebooksPanel";
|
||||
|
||||
describe("Setup Notebooks Panel", () => {
|
||||
it("should render Default properly", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
panelTitle: "",
|
||||
panelDescription: "",
|
||||
};
|
||||
const wrapper = mount(<SetupNoteBooksPanel {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render button", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
panelTitle: "",
|
||||
panelDescription: "",
|
||||
};
|
||||
const wrapper = mount(<SetupNoteBooksPanel {...props} />);
|
||||
const button = wrapper.find("PrimaryButton").first();
|
||||
expect(button).toBeDefined();
|
||||
});
|
||||
|
||||
it("Button onClick should call onCompleteSetup", () => {
|
||||
const onCompleteSetupClick = jest.fn();
|
||||
const wrapper = mount(<PrimaryButton onClick={onCompleteSetupClick} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
|
||||
expect(onCompleteSetupClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Button onKeyPress should call onCompleteSetupKeyPress", () => {
|
||||
const onCompleteSetupKeyPress = jest.fn();
|
||||
const wrapper = mount(<PrimaryButton onKeyPress={onCompleteSetupKeyPress} />);
|
||||
wrapper.find("button").simulate("keypress");
|
||||
|
||||
expect(onCompleteSetupKeyPress).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import React, { FunctionComponent, KeyboardEvent, useState } from "react";
|
||||
import { Areas, NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { createOrUpdate } from "../../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||
interface SetupNoteBooksPanelProps {
|
||||
explorer: Explorer;
|
||||
panelTitle: string;
|
||||
panelDescription: string;
|
||||
}
|
||||
|
||||
export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> = ({
|
||||
explorer,
|
||||
panelTitle,
|
||||
panelDescription,
|
||||
}: SetupNoteBooksPanelProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
|
||||
const description = panelDescription;
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
|
||||
|
||||
const onCompleteSetupClick = async () => {
|
||||
await setupNotebookWorkspace();
|
||||
};
|
||||
|
||||
const onCompleteSetupKeyPress = async (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === " " || event.key === NormalizedEventKey.Enter) {
|
||||
await setupNotebookWorkspace();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const setupNotebookWorkspace = async (): Promise<void> => {
|
||||
if (!explorer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNotebookWorkspace, {
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: panelTitle,
|
||||
});
|
||||
|
||||
const clear = NotificationConsoleUtils.logConsoleProgress("Creating a new default notebook workspace");
|
||||
|
||||
try {
|
||||
setLoadingTrue();
|
||||
await createOrUpdate(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
explorer.refreshExplorer();
|
||||
|
||||
closeSidePanel();
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.CreateNotebookWorkspace,
|
||||
{
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: panelTitle,
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleInfo("Successfully created a default notebook workspace for the account");
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateNotebookWorkspace,
|
||||
{
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: panelTitle,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
setErrorMessage(`Failed to setup a default notebook workspace: ${errorMessage}`);
|
||||
setShowErrorDetails(true);
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to create a default notebook workspace: ${errorMessage}`);
|
||||
} finally {
|
||||
setLoadingFalse();
|
||||
clear();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="panelFormWrapper">
|
||||
{errorMessage && (
|
||||
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
||||
)}
|
||||
<div className="panelMainContent">
|
||||
<div className="pkPadding">
|
||||
<div>{description}</div>
|
||||
<PrimaryButton
|
||||
id="completeSetupBtn"
|
||||
className="btncreatecoll1 btnSetupQueries"
|
||||
text="Complete Setup"
|
||||
onClick={onCompleteSetupClick}
|
||||
onKeyPress={onCompleteSetupKeyPress}
|
||||
aria-label="Complete setup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && <PanelLoadingScreen />}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -215,13 +215,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
{
|
||||
metric: "Request Charge",
|
||||
value: this.state.requestChargeDisplayText,
|
||||
toolTip: "",
|
||||
toolTip: "Request Charge",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
metric: "Showing Results",
|
||||
value: this.state.showingDocumentsDisplayText,
|
||||
toolTip: "",
|
||||
toolTip: "Showing Results",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25,7 +25,8 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Computed<boolean>;
|
||||
constructor(
|
||||
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
||||
private getDatabaseAccount: () => DataModels.DatabaseAccount
|
||||
private getDatabaseAccount: () => DataModels.DatabaseAccount,
|
||||
private getTabId: () => string
|
||||
) {}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
@@ -33,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={this.getNotebookServerInfo()}
|
||||
databaseAccount={this.getDatabaseAccount()}
|
||||
tabId={this.getTabId()}
|
||||
/>
|
||||
) : (
|
||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
||||
@@ -50,7 +52,8 @@ export default class TerminalTab extends TabsBase {
|
||||
this.container = options.container;
|
||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||
() => this.getNotebookServerInfo(options),
|
||||
() => userContext?.databaseAccount
|
||||
() => userContext?.databaseAccount,
|
||||
() => this.tabId
|
||||
);
|
||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
if (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ko from "knockout";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import { GetGithubClientId } from "Utils/GitHubUtils";
|
||||
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
@@ -484,7 +485,7 @@ export class JunoClient {
|
||||
// public for tests
|
||||
public static getJunoEndpoint(): string {
|
||||
const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
if (configContext.allowedJunoOrigins.indexOf(new URL(junoEndpoint).origin) === -1) {
|
||||
if (!validateEndpoint(junoEndpoint, allowedJunoOrigins)) {
|
||||
const error = `${junoEndpoint} not allowed as juno endpoint`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import {
|
||||
Areas,
|
||||
ConnectionStatusType,
|
||||
@@ -154,7 +155,7 @@ export class PhoenixClient {
|
||||
public static getPhoenixEndpoint(): string {
|
||||
const phoenixEndpoint =
|
||||
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
|
||||
if (!validateEndpoint(phoenixEndpoint, allowedJunoOrigins)) {
|
||||
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
|
||||
@@ -23,7 +23,6 @@ export type Features = {
|
||||
readonly hostedDataExplorer: boolean;
|
||||
readonly junoEndpoint?: string;
|
||||
readonly phoenixEndpoint?: string;
|
||||
readonly livyEndpoint?: string;
|
||||
readonly notebookBasePath?: string;
|
||||
readonly notebookServerToken?: string;
|
||||
readonly notebookServerUrl?: string;
|
||||
@@ -72,7 +71,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
mongoProxyAPIs: get("mongoproxyapis"),
|
||||
junoEndpoint: get("junoendpoint"),
|
||||
phoenixEndpoint: get("phoenixendpoint"),
|
||||
livyEndpoint: get("livyendpoint"),
|
||||
notebookBasePath: get("notebookbasepath"),
|
||||
notebookServerToken: get("notebookservertoken"),
|
||||
notebookServerUrl: get("notebookserverurl"),
|
||||
|
||||
@@ -2,15 +2,61 @@
|
||||
* JupyterLab applications based on jupyterLab components
|
||||
*/
|
||||
import { ServerConnection, TerminalManager } from "@jupyterlab/services";
|
||||
import { IMessage } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import { Terminal } from "@jupyterlab/terminal";
|
||||
import { Panel, Widget } from "@phosphor/widgets";
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
export class JupyterLabAppFactory {
|
||||
public static async createTerminalApp(serverSettings: ServerConnection.ISettings) {
|
||||
private isShellStarted: boolean | undefined;
|
||||
private checkShellStarted: ((content: string | undefined) => void) | undefined;
|
||||
private onShellExited: () => void;
|
||||
|
||||
private isShellExited(content: string | undefined) {
|
||||
return content?.includes("cosmosuser@");
|
||||
}
|
||||
|
||||
private isMongoShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("MongoDB shell version");
|
||||
}
|
||||
|
||||
private isCassandraShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("Connected to") && content?.includes("cqlsh");
|
||||
}
|
||||
|
||||
constructor(closeTab: () => void) {
|
||||
this.onShellExited = closeTab;
|
||||
this.isShellStarted = false;
|
||||
this.checkShellStarted = undefined;
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
this.checkShellStarted = this.isMongoShellStarted;
|
||||
break;
|
||||
case "Cassandra":
|
||||
this.checkShellStarted = this.isCassandraShellStarted;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings) {
|
||||
const manager = new TerminalManager({
|
||||
serverSettings: serverSettings,
|
||||
});
|
||||
const session = await manager.startNew();
|
||||
session.messageReceived.connect(async (_, message: IMessage) => {
|
||||
const content = message.content && message.content[0]?.toString();
|
||||
|
||||
if (this.checkShellStarted && message.type == "stdout") {
|
||||
//Close the terminal tab once the shell closed messages are received
|
||||
if (!this.isShellStarted) {
|
||||
this.checkShellStarted(content);
|
||||
} else if (this.isShellExited(content)) {
|
||||
this.onShellExited();
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
const term = new Terminal(session, { theme: "dark", shutdownOnClose: true });
|
||||
|
||||
if (!term) {
|
||||
|
||||
@@ -10,4 +10,5 @@ export interface TerminalProps {
|
||||
authType: AuthType;
|
||||
apiType: ApiType;
|
||||
subscriptionId: string;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ServerConnection } from "@jupyterlab/services";
|
||||
import "@jupyterlab/terminal/style/index.css";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import postRobot from "post-robot";
|
||||
import { HttpHeaders } from "../Common/Constants";
|
||||
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -54,13 +55,20 @@ const initTerminal = async (props: TerminalProps) => {
|
||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||
|
||||
try {
|
||||
await JupyterLabAppFactory.createTerminalApp(serverSettings);
|
||||
await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
|
||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
||||
}
|
||||
};
|
||||
|
||||
const closeTab = (tabId: string): void => {
|
||||
window.parent.postMessage(
|
||||
{ type: MessageTypes.CloseTab, data: { tabId: tabId }, signature: "pcIframe" },
|
||||
window.document.referrer
|
||||
);
|
||||
};
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
postRobot.on(
|
||||
"props",
|
||||
|
||||
87
src/Utils/EndpointValidation.ts
Normal file
87
src/Utils/EndpointValidation.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { JunoEndpoints } from "Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
|
||||
export function validateEndpoint(
|
||||
endpointToValidate: string | undefined,
|
||||
allowedEndpoints: ReadonlyArray<string>
|
||||
): boolean {
|
||||
try {
|
||||
return validateEndpointInternal(
|
||||
endpointToValidate,
|
||||
allowedEndpoints.map((e) => e)
|
||||
);
|
||||
} catch (reason) {
|
||||
Logger.logError(`${endpointToValidate} not allowed`, "validateEndpoint");
|
||||
Logger.logError(`${JSON.stringify(reason)}`, "validateEndpoint");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEndpointInternal(
|
||||
endpointToValidate: string | undefined,
|
||||
allowedEndpoints: ReadonlyArray<string>
|
||||
): boolean {
|
||||
if (endpointToValidate === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const originToValidate: string = new URL(endpointToValidate).origin;
|
||||
const allowedOrigins: string[] = allowedEndpoints.map((allowedEndpoint) => new URL(allowedEndpoint).origin) || [];
|
||||
const valid = allowedOrigins.indexOf(originToValidate) >= 0;
|
||||
|
||||
if (!valid) {
|
||||
throw new Error(
|
||||
`${endpointToValidate} is not an allowed endpoint. Allowed endpoints are ${allowedEndpoints.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
export const allowedArmEndpoints: ReadonlyArray<string> = [
|
||||
"https://management.azure.com",
|
||||
"https://management.usgovcloudapi.net",
|
||||
"https://management.chinacloudapi.cn",
|
||||
];
|
||||
|
||||
export const allowedAadEndpoints: ReadonlyArray<string> = ["https://login.microsoftonline.com/"];
|
||||
|
||||
export const allowedBackendEndpoints: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
"https://localhost:12901",
|
||||
"https://localhost:1234",
|
||||
];
|
||||
|
||||
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
"https://localhost:12901",
|
||||
];
|
||||
|
||||
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
||||
|
||||
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
|
||||
|
||||
export const allowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.windows.net"];
|
||||
|
||||
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];
|
||||
|
||||
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
|
||||
|
||||
export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = [
|
||||
"https://cosmos-explorer-preview.azurewebsites.net/",
|
||||
];
|
||||
|
||||
export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
JunoEndpoints.Test,
|
||||
JunoEndpoints.Test2,
|
||||
JunoEndpoints.Test3,
|
||||
JunoEndpoints.Prod,
|
||||
JunoEndpoints.Stage,
|
||||
"https://localhost",
|
||||
];
|
||||
|
||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
||||
@@ -4,7 +4,7 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
|
||||
return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
|
||||
}
|
||||
|
||||
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
|
||||
function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEvent): boolean {
|
||||
const eventOrigin = (event && event.origin) || "";
|
||||
const windowOrigin = (window && window.origin) || "";
|
||||
if (eventOrigin === windowOrigin) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { applyExplorerBindings } from "../applyExplorerBindings";
|
||||
import { AuthType } from "../AuthType";
|
||||
@@ -69,16 +70,38 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||
|
||||
async function configureHosted(): Promise<Explorer> {
|
||||
const win = (window as unknown) as HostedExplorerChildFrame;
|
||||
let explorer: Explorer;
|
||||
if (win.hostedConfig.authType === AuthType.EncryptedToken) {
|
||||
return configureHostedWithEncryptedToken(win.hostedConfig);
|
||||
explorer = configureHostedWithEncryptedToken(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.ResourceToken) {
|
||||
return configureHostedWithResourceToken(win.hostedConfig);
|
||||
explorer = configureHostedWithResourceToken(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.ConnectionString) {
|
||||
return configureHostedWithConnectionString(win.hostedConfig);
|
||||
explorer = configureHostedWithConnectionString(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.AAD) {
|
||||
return configureHostedWithAAD(win.hostedConfig);
|
||||
explorer = await configureHostedWithAAD(win.hostedConfig);
|
||||
} else {
|
||||
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
|
||||
}
|
||||
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
|
||||
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldProcessMessage(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data?.type === MessageTypes.CloseTab) {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
return explorer;
|
||||
}
|
||||
|
||||
async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
||||
@@ -261,6 +284,8 @@ async function configurePortal(): Promise<Explorer> {
|
||||
}
|
||||
} else if (shouldForwardMessage(message, event.origin)) {
|
||||
sendMessage(message);
|
||||
} else if (event.data?.type === MessageTypes.CloseTab) {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
}
|
||||
},
|
||||
false
|
||||
|
||||
Reference in New Issue
Block a user