Compare commits

..

2 Commits

Author SHA1 Message Date
vaidankarswapnil
ceb5074db3 Updated 1 test snapshot 2022-01-20 13:55:57 +05:30
vaidankarswapnil
bb1bbc2f70 Fixed a11y issue for the settings label 2022-01-20 13:44:29 +05:30
41 changed files with 3211 additions and 9154 deletions

6
.vscode/launch.json vendored
View File

@@ -12,8 +12,7 @@
"--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js",
"--runInBand",
"--coverage",
"false"
"--coverage", "false"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
@@ -27,8 +26,7 @@
"--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

View File

@@ -1,5 +1,4 @@
{
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
"isTerminalEnabled" : true,
"isPhoenixEnabled" : true
"isTerminalEnabled" : true
}

View File

@@ -1,5 +1,4 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isTerminalEnabled" : false,
"isPhoenixEnabled" : false
"isTerminalEnabled" : false
}

View File

@@ -2885,6 +2885,10 @@ a:link {
.settingsSectionPart {
padding-left: 8px;
label {
padding: 0 !important;
font-size: inherit;
}
}
.settingsSectionLabel {

3225
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -99,7 +99,6 @@ export class Flights {
public static readonly PhoenixNotebooks = "phoenixnotebooks";
public static readonly PhoenixFeatures = "phoenixfeatures";
public static readonly NotebooksDownBanner = "notebooksdownbanner";
public static readonly FreeTierAutoscaleThroughput = "freetierautoscalethroughput";
}
export class AfecFeatures {

View File

@@ -236,12 +236,13 @@ describe("MongoProxyClient", () => {
});
it("returns a production endpoint", () => {
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
const endpoint = getEndpoint();
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
});
it("returns a development endpoint", () => {
const endpoint = getEndpoint("https://localhost:1234");
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
const endpoint = getEndpoint();
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
});
@@ -249,7 +250,7 @@ describe("MongoProxyClient", () => {
updateUserContext({
authType: AuthType.EncryptedToken,
});
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
const endpoint = getEndpoint();
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
});
});

View File

@@ -1,6 +1,5 @@
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";
@@ -337,17 +336,14 @@ export function createMongoCollectionWithProxy(
}
export function getFeatureEndpointOrDefault(feature: string): string {
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);
return hasFlag(userContext.features.mongoProxyAPIs, feature)
? getEndpoint(userContext.features.mongoProxyEndpoint)
: getEndpoint();
}
export function getEndpoint(endpoint: string): string {
let url = endpoint + "/api/mongo/explorer";
export function getEndpoint(customEndpoint?: string): string {
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
url += "/api/mongo/explorer";
if (userContext.authType === AuthType.EncryptedToken) {
url = url.replace("api/mongo", "api/guest/mongo");

View File

@@ -1,16 +1,4 @@
import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
allowedArmEndpoints,
allowedBackendEndpoints,
allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints,
allowedJunoOrigins,
allowedMongoBackendEndpoints,
allowedMsalRedirectEndpoints,
validateEndpoint,
} from "Utils/EndpointValidation";
import { JunoEndpoints } from "Common/Constants";
export enum Platform {
Portal = "Portal",
@@ -20,7 +8,7 @@ export enum Platform {
export interface ConfigContext {
platform: Platform;
allowedParentFrameOrigins: ReadonlyArray<string>;
allowedParentFrameOrigins: string[];
gitSha?: string;
proxyPath?: string;
AAD_ENDPOINT: string;
@@ -40,9 +28,9 @@ export interface ConfigContext {
GITHUB_TEST_ENV_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
isTerminalEnabled: boolean;
isPhoenixEnabled: boolean;
hostedExplorerURL: string;
armAPIVersion?: string;
allowedJunoOrigins: string[];
msalRedirectURI?: string;
}
@@ -52,11 +40,12 @@ let configContext: Readonly<ConfigContext> = {
allowedParentFrameOrigins: [
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`,
`^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
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`,
],
// Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
AAD_ENDPOINT: "https://login.microsoftonline.com/",
@@ -72,7 +61,14 @@ let configContext: Readonly<ConfigContext> = {
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
isTerminalEnabled: false,
isPhoenixEnabled: false,
allowedJunoOrigins: [
JunoEndpoints.Test,
JunoEndpoints.Test2,
JunoEndpoints.Test3,
JunoEndpoints.Prod,
JunoEndpoints.Stage,
"https://localhost",
],
};
export function resetConfigContext(): void {
@@ -83,50 +79,6 @@ 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);
}
@@ -150,8 +102,18 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
});
if (response.status === 200) {
try {
const { ...externalConfig } = await response.json();
updateConfigContext(externalConfig);
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],
});
}
} catch (error) {
console.error("Unable to parse json in config file");
console.error(error);

View File

@@ -19,7 +19,7 @@ import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/Telemet
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../../../UserContext";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import { autoPilotThroughput1K, autoPilotThroughput4K } from "../../../../../Utils/AutoPilotUtils";
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
@@ -540,7 +540,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
step={AutoPilotUtils.autoPilotIncrementStep}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
min={userContext.features.freetierAutoscaleThroughput ? autoPilotThroughput1K : autoPilotThroughput4K}
min={minAutoPilotThroughput}
errorMessage={this.props.throughputError}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}

View File

@@ -5,7 +5,6 @@ const props = {
isDatabase: false,
showFreeTierExceedThroughputTooltip: true,
isSharded: true,
isFreeTier: false,
setThroughputValue: () => jest.fn(),
setIsAutoscale: () => jest.fn(),
setIsThroughputCapExceeded: () => jest.fn(),

View File

@@ -14,7 +14,6 @@ import "./ThroughputInput.less";
export interface ThroughputInputProps {
isDatabase: boolean;
isSharded: boolean;
isFreeTier: boolean;
showFreeTierExceedThroughputTooltip: boolean;
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
@@ -24,20 +23,15 @@ export interface ThroughputInputProps {
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
isDatabase,
isSharded,
isFreeTier,
showFreeTierExceedThroughputTooltip,
setThroughputValue,
setIsAutoscale,
setIsThroughputCapExceeded,
isSharded,
onCostAcknowledgeChange,
}: ThroughputInputProps) => {
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(
isFreeTier && userContext.features.freetierAutoscaleThroughput
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K
);
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>("");
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
@@ -157,15 +151,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") {
const defaultThroughput =
isFreeTier && userContext.features.freetierAutoscaleThroughput
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K;
setThroughput(defaultThroughput);
setThroughput(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoScaleSelected(true);
setThroughputValue(defaultThroughput);
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoscale(true);
checkThroughputCap(defaultThroughput);
checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput);
} else {
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoScaleSelected(false);
@@ -236,11 +226,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
}}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={
userContext.features.freetierAutoscaleThroughput
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K
}
min={AutoPilotUtils.minAutoPilotThroughput}
value={throughput.toString()}
aria-label="Max request units per second"
required={true}

View File

@@ -3,7 +3,6 @@
exports[`ThroughputInput Pane should render Default properly 1`] = `
<ThroughputInput
isDatabase={false}
isFreeTier={false}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}

View File

@@ -1,10 +1,8 @@
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";
@@ -26,6 +24,7 @@ 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";
@@ -33,7 +32,11 @@ 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 { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import {
get as getWorkspace,
listByDatabaseAccount,
start,
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
@@ -47,12 +50,13 @@ 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 { NotebookPaneContent } from "./Notebook/NotebookManager";
import type { 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";
@@ -174,11 +178,7 @@ export default class Explorer {
this.resourceTree = new ResourceTreeAdapter(this);
// Override notebook server parameters from URL parameters
if (
userContext.features.notebookServerUrl &&
validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
userContext.features.notebookServerToken
) {
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl,
authToken: userContext.features.notebookServerToken,
@@ -190,6 +190,19 @@ 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();
}
@@ -386,12 +399,6 @@ export default class Explorer {
});
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetContainerConnection(connectionStatus);
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket."
);
throw error;
} finally {
useNotebook.getState().setIsAllocating(false);
@@ -415,10 +422,7 @@ export default class Explorer {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint:
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
userContext.features.notebookServerUrl) ||
connectionInfo.data.notebookServerUrl,
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
forwardingId: connectionInfo.data.forwardingId,
});
@@ -468,6 +472,35 @@ 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");
@@ -1154,12 +1187,20 @@ 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
@@ -1244,10 +1285,7 @@ export default class Explorer {
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled =
userContext.features.notebooksDownBanner ||
useNotebook.getState().isPhoenixNotebooks ||
useNotebook.getState().isPhoenixFeatures;
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenixNotebooks;
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook
.getState()

View File

@@ -10,6 +10,7 @@ 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";
@@ -34,6 +35,7 @@ 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";
@@ -109,6 +111,11 @@ export function createStaticCommandBarButtons(
}
buttons.push(btn);
});
} else {
if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenixNotebooks) {
buttons.push(createDivider());
buttons.push(createEnableNotebooksButton(container));
}
}
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
@@ -459,6 +466,33 @@ 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 {
@@ -476,6 +510,9 @@ 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 {
@@ -484,6 +521,13 @@ 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,
@@ -498,6 +542,9 @@ 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 {
@@ -506,6 +553,13 @@ 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,

View File

@@ -19,7 +19,6 @@ export class NotebookContainerClient {
private isResettingWorkspace: boolean;
private phoenixClient: PhoenixClient;
private retryOptions: promiseRetry.Options;
private scheduleTimerId: NodeJS.Timeout;
constructor(private onConnectionLost: () => void) {
this.phoenixClient = new PhoenixClient();
@@ -28,36 +27,36 @@ export class NotebookContainerClient {
maxTimeout: Notebook.retryAttemptDelayMs,
minTimeout: Notebook.retryAttemptDelayMs,
};
this.initHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
private initHeartbeat(delayMs: number): void {
this.scheduleHeartbeat(delayMs);
useNotebook.subscribe(
() => this.scheduleHeartbeat(delayMs),
(state) => state.notebookServerInfo
);
}
private scheduleHeartbeat(delayMs: number) {
if (this.scheduleTimerId) {
clearInterval(this.scheduleTimerId);
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleTimerId = setInterval(async () => {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
const memoryUsageInfo = await this.getMemoryUsage();
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
}
}, delayMs);
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
} else {
const unsub = useNotebook.subscribe(
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
if (newServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
unsub();
},
(state) => state.notebookServerInfo
);
}
}
/**
* Heartbeat: each ping schedules another ping
*/
private scheduleHeartbeat(delayMs: number): void {
setTimeout(async () => {
const memoryUsageInfo = await this.getMemoryUsage();
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
}, delayMs);
}
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {

View File

@@ -303,8 +303,8 @@ export class NotebookContentClient {
private getServerConfig(): ServerConfig {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
return {
endpoint: notebookServerInfo?.notebookServerEndpoint,
token: notebookServerInfo?.authToken,
endpoint: notebookServerInfo.notebookServerEndpoint,
token: notebookServerInfo.authToken,
crossDomain: true,
};
}

View File

@@ -249,7 +249,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true}
isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
@@ -484,7 +483,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false}
isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>

View File

@@ -146,10 +146,9 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
// TODO add feature flag that disables validation for customers with custom accounts
if (isAutoscaleSelected) {
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
const minAutoPilotThroughput = userContext.features.freetierAutoscaleThroughput
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K;
setFormErrors(`Please enter a value greater than ${minAutoPilotThroughput} for autopilot throughput`);
setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
}
@@ -242,7 +241,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
isFreeTier={isFreeTierAccount}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}

View File

@@ -262,7 +262,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
}
isDatabase
isSharded
isFreeTier={isFreeTierAccount}
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
@@ -336,7 +335,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false}
isSharded
isFreeTier={isFreeTierAccount}
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}

View File

@@ -10,6 +10,7 @@ import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUti
import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { useNotebook } from "../../Notebook/useNotebook";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
@@ -103,14 +104,11 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
switch (location.type) {
case "MyNotebooks":
parent = {
name: useNotebook.getState().notebookFolderName,
name: ResourceTreeAdapter.MyNotebooksTitle,
path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory,
};
isGithubTree = false;
if (useNotebook.getState().isPhoenixNotebooks) {
await container.allocateContainer();
}
break;
case "GitHub":

View File

@@ -188,14 +188,11 @@ export const SettingsPane: FunctionComponent = () => {
{shouldShowCrossPartitionOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Enable cross-partition query
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is
not scoped to single partition key value.
</InfoTooltip>
</div>
<label className="settingsSectionLabel">Enable cross-partition query</label>
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is
not scoped to single partition key value.
</InfoTooltip>
<Checkbox
styles={{
label: { padding: 0 },
@@ -211,14 +208,14 @@ export const SettingsPane: FunctionComponent = () => {
{shouldShowParallelismOption && (
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
<label className="settingsSectionLabel" htmlFor="input65">
Max degree of parallelism
<InfoTooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A
positive property value limits the number of concurrent operations to the set value. If it is set to
less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
</div>
</label>
<InfoTooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A
positive property value limits the number of concurrent operations to the set value. If it is set to
less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
<SpinButton
min={-1}

View File

@@ -103,14 +103,14 @@ exports[`Settings Pane should render Default properly 1`] = `
<div
className="settingsSectionPart"
>
<div
<label
className="settingsSectionLabel"
>
Enable cross-partition query
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
</InfoTooltip>
</div>
</label>
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
</InfoTooltip>
<StyledCheckboxBase
ariaLabel="Enable cross partition query"
checked={true}
@@ -132,14 +132,15 @@ exports[`Settings Pane should render Default properly 1`] = `
<div
className="settingsSectionPart"
>
<div
<label
className="settingsSectionLabel"
htmlFor="input65"
>
Max degree of parallelism
<InfoTooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
</div>
</label>
<InfoTooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
<StyledSpinButton
ariaLabel="Max degree of parallelism"
className="textfontclr"

View File

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

View File

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

View File

@@ -35,60 +35,6 @@ const {
Smallint,
Tinyint,
Timestamp,
// List
List_Ascii,
List_Bigint,
List_Blob,
List_Boolean,
List_Date,
List_Decimal,
List_Double,
List_Float,
List_Int,
List_Text,
List_Timestamp,
List_Uuid,
List_Varchar,
List_Varint,
List_Inet,
List_Smallint,
List_Tinyint,
// Map
Map_Ascii,
Map_Bigint,
Map_Blob,
Map_Boolean,
Map_Date,
Map_Decimal,
Map_Double,
Map_Float,
Map_Int,
Map_Text,
Map_Timestamp,
Map_Uuid,
Map_Varchar,
Map_Varint,
Map_Inet,
Map_Smallint,
Map_Tinyint,
// Set
Set_Ascii,
Set_Bigint,
Set_Blob,
Set_Boolean,
Set_Date,
Set_Decimal,
Set_Double,
Set_Float,
Set_Int,
Set_Text,
Set_Timestamp,
Set_Uuid,
Set_Varchar,
Set_Varint,
Set_Inet,
Set_Smallint,
Set_Tinyint,
} = TableConstants.CassandraType;
export const cassandraOptions = [
{ key: Text, text: Text },
@@ -108,60 +54,6 @@ export const cassandraOptions = [
{ key: Smallint, text: Smallint },
{ key: Tinyint, text: Tinyint },
{ key: Timestamp, text: Timestamp },
// List
{ key: List_Ascii, text: List_Ascii },
{ key: List_Bigint, text: List_Bigint },
{ key: List_Blob, text: List_Blob },
{ key: List_Boolean, text: List_Boolean },
{ key: List_Date, text: List_Date },
{ key: List_Decimal, text: List_Decimal },
{ key: List_Double, text: List_Double },
{ key: List_Float, text: List_Float },
{ key: List_Int, text: List_Int },
{ key: List_Text, text: List_Text },
{ key: List_Timestamp, text: List_Timestamp },
{ key: List_Uuid, text: List_Uuid },
{ key: List_Varchar, text: List_Varchar },
{ key: List_Varint, text: List_Varint },
{ key: List_Inet, text: List_Inet },
{ key: List_Smallint, text: List_Smallint },
{ key: List_Tinyint, text: List_Tinyint },
// Map
{ key: Map_Ascii, text: Map_Ascii },
{ key: Map_Bigint, text: Map_Bigint },
{ key: Map_Blob, text: Map_Blob },
{ key: Map_Boolean, text: Map_Boolean },
{ key: Map_Date, text: Map_Date },
{ key: Map_Decimal, text: Map_Decimal },
{ key: Map_Double, text: Map_Double },
{ key: Map_Float, text: Map_Float },
{ key: Map_Int, text: Map_Int },
{ key: Map_Text, text: Map_Text },
{ key: Map_Timestamp, text: Map_Timestamp },
{ key: Map_Uuid, text: Map_Uuid },
{ key: Map_Varchar, text: Map_Varchar },
{ key: Map_Varint, text: Map_Varint },
{ key: Map_Inet, text: Map_Inet },
{ key: Map_Smallint, text: Map_Smallint },
{ key: Map_Tinyint, text: Map_Tinyint },
// Set
{ key: Set_Ascii, text: Set_Ascii },
{ key: Set_Bigint, text: Set_Bigint },
{ key: Set_Blob, text: Set_Blob },
{ key: Set_Boolean, text: Set_Boolean },
{ key: Set_Date, text: Set_Date },
{ key: Set_Decimal, text: Set_Decimal },
{ key: Set_Double, text: Set_Double },
{ key: Set_Float, text: Set_Float },
{ key: Set_Int, text: Set_Int },
{ key: Set_Text, text: Set_Text },
{ key: Set_Timestamp, text: Set_Timestamp },
{ key: Set_Uuid, text: Set_Uuid },
{ key: Set_Varchar, text: Set_Varchar },
{ key: Set_Varint, text: Set_Varint },
{ key: Set_Inet, text: Set_Inet },
{ key: Set_Smallint, text: Set_Smallint },
{ key: Set_Tinyint, text: Set_Tinyint },
];
export const imageProps: IImageProps = {

View File

@@ -27,60 +27,6 @@ export const CassandraType = {
Inet: "Inet",
Smallint: "Smallint",
Tinyint: "Tinyint",
List_Ascii: "List<Ascii>",
List_Bigint: "List<Bigint>",
List_Blob: "List<Blob>",
List_Boolean: "List<Boolean>",
List_Date: "List<Date>",
List_Decimal: "List<Decimal>",
List_Double: "List<Double>",
List_Float: "List<Float>",
List_Int: "List<Int>",
List_Text: "List<Text>",
List_Timestamp: "List<Timestamp>",
List_Uuid: "List<Uuid>",
List_Varchar: "List<Varchar>",
List_Varint: "List<Varint>",
List_Inet: "List<Inet>",
List_Smallint: "List<Smallint>",
List_Tinyint: "List<Tinyint>",
Map_Ascii: "Map<Ascii>",
Map_Bigint: "Map<Bigint>",
Map_Blob: "Map<Blob>",
Map_Boolean: "Map<Boolean>",
Map_Date: "Map<Date>",
Map_Decimal: "Map<Decimal>",
Map_Double: "Map<Double>",
Map_Float: "Map<Float>",
Map_Int: "Map<Int>",
Map_Text: "Map<Text>",
Map_Timestamp: "Map<Timestamp>",
Map_Uuid: "Map<Uuid>",
Map_Varchar: "Map<Varchar>",
Map_Varint: "Map<Varint>",
Map_Inet: "Map<Inet>",
Map_Smallint: "Map<Smallint>",
Map_Tinyint: "Map<Tinyint>",
Set_Ascii: "Set<Ascii>",
Set_Bigint: "Set<Bigint>",
Set_Blob: "Set<Blob>",
Set_Boolean: "Set<Boolean>",
Set_Date: "Set<Date>",
Set_Decimal: "Set<Decimal>",
Set_Double: "Set<Double>",
Set_Float: "Set<Float>",
Set_Int: "Set<Int>",
Set_Text: "Set<Text>",
Set_Timestamp: "Set<Timestamp>",
Set_Uuid: "Set<Uuid>",
Set_Varchar: "Set<Varchar>",
Set_Varint: "Set<Varint>",
Set_Inet: "Set<Inet>",
Set_Smallint: "Set<Smallint>",
Set_Tinyint: "Set<Tinyint>",
};
export const ClauseRule = {

View File

@@ -215,13 +215,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
{
metric: "Request Charge",
value: this.state.requestChargeDisplayText,
toolTip: "Request Charge",
toolTip: "",
isQueryMetricsEnabled: true,
},
{
metric: "Showing Results",
value: this.state.showingDocumentsDisplayText,
toolTip: "Showing Results",
toolTip: "",
isQueryMetricsEnabled: true,
},
{

View File

@@ -308,7 +308,7 @@ export default class Collection implements ViewModels.Collection {
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.rawDataModel.id + " - Items",
tabTitle: "Items",
});
this.documentIds([]);
@@ -316,7 +316,7 @@ export default class Collection implements ViewModels.Collection {
partitionKey: this.partitionKey,
documentIds: ko.observableArray<DocumentId>([]),
tabKind: ViewModels.CollectionTabKind.Documents,
title: this.rawDataModel.id + " - Items",
title: "Items",
collection: this,
node: this,
tabPath: `${this.databaseId}>${this.id()}>Documents`,

View File

@@ -1,5 +1,4 @@
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";
@@ -485,7 +484,7 @@ export class JunoClient {
// public for tests
public static getJunoEndpoint(): string {
const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (!validateEndpoint(junoEndpoint, allowedJunoOrigins)) {
if (configContext.allowedJunoOrigins.indexOf(new URL(junoEndpoint).origin) === -1) {
const error = `${junoEndpoint} not allowed as juno endpoint`;
console.error(error);
throw new Error(error);

View File

@@ -1,6 +1,5 @@
import promiseRetry, { AbortError } from "p-retry";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
import {
Areas,
ConnectionStatusType,
@@ -155,7 +154,7 @@ export class PhoenixClient {
public static getPhoenixEndpoint(): string {
const phoenixEndpoint =
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (!validateEndpoint(phoenixEndpoint, allowedJunoOrigins)) {
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
console.error(error);
throw new Error(error);

View File

@@ -1,5 +1,4 @@
export type Features = {
// set only via feature flags
readonly canExceedMaximumValue: boolean;
readonly cosmosdb: boolean;
readonly enableChangeFeedPolicy: boolean;
@@ -9,6 +8,12 @@ export type Features = {
readonly enableReactPane: boolean;
readonly enableRightPanelV2: boolean;
readonly enableSchema: boolean;
autoscaleDefault: boolean;
partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
phoenixNotebooks: boolean;
phoenixFeatures: boolean;
notebooksDownBanner: boolean;
readonly enableSDKoperations: boolean;
readonly enableSpark: boolean;
readonly enableTtl: boolean;
@@ -18,6 +23,7 @@ export type Features = {
readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string;
readonly phoenixEndpoint?: string;
readonly livyEndpoint?: string;
readonly notebookBasePath?: string;
readonly notebookServerToken?: string;
readonly notebookServerUrl?: string;
@@ -29,22 +35,11 @@ export type Features = {
readonly mongoProxyEndpoint?: string;
readonly mongoProxyAPIs?: string;
readonly enableThroughputCap: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
phoenixNotebooks?: boolean;
phoenixFeatures?: boolean;
notebooksDownBanner: boolean;
freetierAutoscaleThroughput: boolean;
};
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
const downcased = new URLSearchParams();
const set = (value: string, key: string) => {
downcased.set(key.toLowerCase(), value);
};
const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value);
const get = (key: string, defaultValue?: string) =>
downcased.get("feature." + key) ?? downcased.get(key) ?? defaultValue;
@@ -77,6 +72,7 @@ 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"),
@@ -88,9 +84,10 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
autoscaleDefault: "true" === get("autoscaledefault"),
partitionKeyDefault: "true" === get("partitionkeytest"),
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
phoenixNotebooks: "true" === get("phoenixnotebooks"),
phoenixFeatures: "true" === get("phoenixfeatures"),
notebooksDownBanner: "true" === get("notebooksDownBanner"),
enableThroughputCap: "true" === get("enablethroughputcap"),
freetierAutoscaleThroughput: "true" === get("freetierautoscalethroughput"),
};
}

View File

@@ -1,16 +1,11 @@
import { userContext } from "UserContext";
export const minAutoPilotThroughput = 4000;
export const autoPilotThroughput1K = 1000;
export const autoPilotIncrementStep = 1000;
export const autoPilotThroughput4K = 4000;
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
if (!maxThroughput) {
return false;
}
const minAutoPilotThroughput = userContext.features.freetierAutoscaleThroughput
? autoPilotThroughput4K
: autoPilotThroughput1K;
if (maxThroughput < minAutoPilotThroughput) {
return false;
}

View File

@@ -1,87 +0,0 @@
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> = [];

View File

@@ -2,28 +2,26 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "./MessageValidation"
describe("isInvalidParentFrameOrigin", () => {
test.each`
domain | expected
${"https://cosmos.azure.com"} | ${false}
${"https://cosmos.azure.us"} | ${false}
${"https://cosmos.azure.cn"} | ${false}
${"https://portal.azure.com"} | ${false}
${"https://portal.azure.us"} | ${false}
${"https://portal.azure.cn"} | ${false}
${"https://portal.microsoftazure.de"} | ${false}
${"https://subdomain.portal.azure.com"} | ${false}
${"https://subdomain.portal.azure.us"} | ${false}
${"https://subdomain.portal.azure.cn"} | ${false}
${"https://main.documentdb.ext.azure.com"} | ${false}
${"https://main.documentdb.ext.azure.us"} | ${false}
${"https://main.documentdb.ext.azure.cn"} | ${false}
${"https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de"} | ${false}
${"https://main.documentdb.ext.microsoftazure.de"} | ${false}
${"https://random.domain"} | ${true}
${"https://malicious.cloudapp.azure.com"} | ${true}
${"https://malicious.germanycentral.cloudapp.microsoftazure.de"} | ${true}
${"https://maliciousazure.com"} | ${true}
${"https://maliciousportalsazure.com"} | ${true}
${"https://cosmos-db-dataexplorer-germanycentralAazurewebsites.de"} | ${true}
domain | expected
${"https://cosmos.azure.com"} | ${false}
${"https://cosmos.azure.us"} | ${false}
${"https://cosmos.azure.cn"} | ${false}
${"https://portal.azure.com"} | ${false}
${"https://portal.azure.us"} | ${false}
${"https://portal.azure.cn"} | ${false}
${"https://portal.microsoftazure.de"} | ${false}
${"https://subdomain.portal.azure.com"} | ${false}
${"https://subdomain.portal.azure.us"} | ${false}
${"https://subdomain.portal.azure.cn"} | ${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}
${"https://malicious.germanycentral.cloudapp.microsoftazure.de"} | ${true}
${"https://maliciousazure.com"} | ${true}
${"https://maliciousportalsazure.com"} | ${true}
`("returns $expected when called with $domain", ({ domain, expected }) => {
expect(isInvalidParentFrameOrigin({ origin: domain } as MessageEvent)).toBe(expected);
});

View File

@@ -4,7 +4,7 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
}
function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEvent): boolean {
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
const eventOrigin = (event && event.origin) || "";
const windowOrigin = (window && window.origin) || "";
if (eventOrigin === windowOrigin) {

View File

@@ -351,11 +351,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.features) {
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
}
//Updating phoenix feature flags for MPAC based of config context
if (configContext.isPhoenixEnabled === true) {
userContext.features.phoenixNotebooks = true;
userContext.features.phoenixFeatures = true;
}
if (inputs.flights) {
if (inputs.flights.indexOf(Flights.AutoscaleTest) !== -1) {
userContext.features.autoscaleDefault;
@@ -378,9 +373,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) {
userContext.features.notebooksDownBanner = true;
}
if (inputs.flights.indexOf(Flights.FreeTierAutoscaleThroughput) !== -1) {
userContext.features.freetierAutoscaleThroughput = true;
}
}
}