mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-01 23:32:02 +00:00
Compare commits
9 Commits
dependabot
...
users/kche
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29cdfbd3c3 | ||
|
|
7862946d1c | ||
|
|
0ec6495e69 | ||
|
|
b9dffdd990 | ||
|
|
13811b1d44 | ||
|
|
1643ce4dbb | ||
|
|
7abd65ac4b | ||
|
|
c731eb9cf9 | ||
|
|
c534b2d74b |
@@ -354,6 +354,10 @@ export enum ContainerStatusType {
|
||||
Disconnected = "Disconnected",
|
||||
}
|
||||
|
||||
export enum PoolIdType {
|
||||
DefaultPoolId = "default",
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||
@@ -394,6 +398,10 @@ export class Notebook {
|
||||
public static cosmosNotebookHomePageUrl = "https://aka.ms/cosmos-notebooks-limits";
|
||||
public static cosmosNotebookGitDocumentationUrl = "https://aka.ms/cosmos-notebooks-github";
|
||||
public static learnMore = "Learn more.";
|
||||
public static notebookDisabledText =
|
||||
"This feature is disabled for this user, this can happen because of region restriction, key permissions etc..";
|
||||
public static newShellDisabledText =
|
||||
"This feature is disabled for this user, this is for users with region restriction, key permissions etc..";
|
||||
}
|
||||
|
||||
export class SparkLibrary {
|
||||
|
||||
@@ -438,18 +438,25 @@ export interface NotebookWorkspaceConnectionInfo {
|
||||
|
||||
export interface ContainerInfo {
|
||||
durationLeftInMinutes: number;
|
||||
notebookServerInfo: NotebookWorkspaceConnectionInfo;
|
||||
phoenixServerInfo: NotebookWorkspaceConnectionInfo;
|
||||
status: ContainerStatusType;
|
||||
}
|
||||
|
||||
export interface IProvisionData {
|
||||
cosmosEndpoint: string;
|
||||
poolId: string;
|
||||
}
|
||||
|
||||
export interface IContainerData {
|
||||
forwardingId: string;
|
||||
}
|
||||
|
||||
export interface IDbAccountAllow {
|
||||
status: number;
|
||||
message?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface IResponse<T> {
|
||||
status: number;
|
||||
data: T;
|
||||
@@ -474,8 +481,8 @@ export interface IMaxUsersPerDbAccountExceeded extends IPhoenixError {
|
||||
}
|
||||
|
||||
export interface IPhoenixConnectionInfoResult {
|
||||
readonly notebookAuthToken?: string;
|
||||
readonly notebookServerUrl?: string;
|
||||
readonly authToken?: string;
|
||||
readonly phoenixServiceUrl?: string;
|
||||
readonly forwardingId?: string;
|
||||
}
|
||||
|
||||
@@ -563,4 +570,5 @@ export enum PhoenixErrorType {
|
||||
RegionNotServicable = "RegionNotServicable",
|
||||
SubscriptionNotAllowed = "SubscriptionNotAllowed",
|
||||
UnknownError = "UnknownError",
|
||||
PhoenixFlightFallback = "PhoenixFlightFallback",
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,8 @@ export interface TreeNode {
|
||||
timestamp?: number;
|
||||
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
|
||||
isLoading?: boolean;
|
||||
isDisabled?: boolean;
|
||||
toolTip?: string;
|
||||
isSelected?: () => boolean;
|
||||
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
|
||||
onExpanded?: () => void;
|
||||
@@ -169,9 +171,15 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
|
||||
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""} ${
|
||||
this.props.node.isDisabled ? "disable" : ""
|
||||
}`}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) =>
|
||||
!this.props.node.isDisabled && this.onNodeClick(event, node)
|
||||
}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
|
||||
!this.props.node.isDisabled && this.onNodeKeyPress(event, node)
|
||||
}
|
||||
role="treeitem"
|
||||
id={node.id}
|
||||
>
|
||||
@@ -184,7 +192,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
{this.renderCollapseExpandIcon(node)}
|
||||
{node.iconSrc && <img className="nodeIcon" src={node.iconSrc} alt="" />}
|
||||
{node.label && (
|
||||
<span className="nodeLabel" title={node.label}>
|
||||
<span className="nodeLabel" title={`${node.toolTip ? node.toolTip : node.label}`}>
|
||||
{node.label}
|
||||
</span>
|
||||
)}
|
||||
@@ -195,7 +203,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
</div>
|
||||
{node.children && (
|
||||
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
|
||||
<div className="nodeChildren" data-test={node.label}>
|
||||
<div className="nodeChildren" data-test={node.label} aria-disabled={node.isDisabled}>
|
||||
{TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => (
|
||||
<TreeNodeComponent
|
||||
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}
|
||||
|
||||
@@ -35,7 +35,7 @@ exports[`TreeComponent renders a simple tree 1`] = `
|
||||
|
||||
exports[`TreeNodeComponent does not render children by default 1`] = `
|
||||
<div
|
||||
className=" main2 nodeItem "
|
||||
className=" main2 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
@@ -136,7 +136,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
||||
|
||||
exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`] = `
|
||||
<div
|
||||
className="nodeClassname main12 nodeItem "
|
||||
className="nodeClassname main12 nodeItem "
|
||||
id="id"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
@@ -287,7 +287,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
|
||||
exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||
<div
|
||||
className=" main2 nodeItem "
|
||||
className=" main2 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
@@ -359,7 +359,7 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||
|
||||
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
||||
<div
|
||||
className="nodeClassname main12 nodeItem "
|
||||
className="nodeClassname main12 nodeItem "
|
||||
id="id"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
@@ -529,7 +529,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
|
||||
exports[`TreeNodeComponent renders unsorted children by default 1`] = `
|
||||
<div
|
||||
className=" main2 nodeItem "
|
||||
className=" main2 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
|
||||
@@ -66,7 +66,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disable{
|
||||
background-color: rgb(255, 255, 255);
|
||||
filter: grayscale();
|
||||
color: rgb(161, 159, 157);
|
||||
}
|
||||
.treeComponentMenuItemContainer {
|
||||
font-size: @mediumFontSize;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import shallow from "zustand/shallow";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
|
||||
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants";
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||
@@ -357,6 +357,7 @@ export default class Explorer {
|
||||
) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
poolId: PoolIdType.DefaultPoolId,
|
||||
};
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
@@ -369,8 +370,8 @@ export default class Explorer {
|
||||
});
|
||||
useNotebook.getState().setIsAllocating(true);
|
||||
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||
if (!connectionInfo?.data?.notebookServerUrl) {
|
||||
throw new Error(`NotebookServerUrl is invalid!`);
|
||||
if (!connectionInfo?.data?.phoenixServiceUrl) {
|
||||
throw new Error(`PhoenixServiceUrl is invalid!`);
|
||||
}
|
||||
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
|
||||
@@ -421,8 +422,8 @@ export default class Explorer {
|
||||
notebookServerEndpoint:
|
||||
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerUrl) ||
|
||||
connectionInfo.data.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||
connectionInfo.data.phoenixServiceUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.authToken,
|
||||
forwardingId: connectionInfo.data.forwardingId,
|
||||
});
|
||||
this.notebookManager?.notebookClient
|
||||
@@ -497,8 +498,8 @@ export default class Explorer {
|
||||
if (connectionInfo?.status !== HttpStatusCodes.OK) {
|
||||
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
|
||||
}
|
||||
if (!connectionInfo?.data?.notebookServerUrl) {
|
||||
throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`);
|
||||
if (!connectionInfo?.data?.phoenixServiceUrl) {
|
||||
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||
@@ -1026,7 +1027,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
||||
if (useNotebook.getState().isPhoenixFeatures) {
|
||||
if (useNotebook.getState().isPhoenixFeatures && !useNotebook.getState().isPhoenixDisabled) {
|
||||
await this.allocateContainer();
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
||||
|
||||
@@ -75,7 +75,11 @@ export function createStaticCommandBarButtons(
|
||||
if (container.notebookManager?.gitHubOAuthService) {
|
||||
notebookButtons.push(createManageGitHubAccountButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
|
||||
if (
|
||||
useNotebook.getState().isPhoenixFeatures &&
|
||||
!useNotebook.getState().isPhoenixDisabled &&
|
||||
configContext.isTerminalEnabled
|
||||
) {
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
|
||||
@@ -83,8 +87,8 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
if (
|
||||
(userContext.apiType === "Mongo" &&
|
||||
useNotebook.getState().isShellEnabled &&
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected() &&
|
||||
useNotebook.getState().isShellEnabled) ||
|
||||
userContext.apiType === "Cassandra"
|
||||
) {
|
||||
notebookButtons.push(createDivider());
|
||||
@@ -96,16 +100,26 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
|
||||
notebookButtons.forEach((btn) => {
|
||||
const isPhoenixFeaturesDownMsg =
|
||||
(!useNotebook.getState().isPhoenixFeatures && !useNotebook.getState().isPhoenixDisabled) ||
|
||||
(!useNotebook.getState().isPhoenixFeatures && useNotebook.getState().isPhoenixDisabled);
|
||||
|
||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
||||
if (isPhoenixFeaturesDownMsg) {
|
||||
applyNotebooksStyleProps(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
||||
} else if (useNotebook.getState().isPhoenixDisabled) {
|
||||
applyNotebooksStyleProps(btn, Constants.Notebook.notebookDisabledText);
|
||||
}
|
||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
||||
if (isPhoenixFeaturesDownMsg) {
|
||||
applyNotebooksStyleProps(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
||||
} else if (useNotebook.getState().isPhoenixDisabled) {
|
||||
applyNotebooksStyleProps(btn, Constants.Notebook.notebookDisabledText);
|
||||
}
|
||||
} else if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
} else if (isPhoenixFeaturesDownMsg) {
|
||||
applyNotebooksStyleProps(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
} else if (useNotebook.getState().isPhoenixDisabled) {
|
||||
applyNotebooksStyleProps(btn, Constants.Notebook.notebookDisabledText);
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
@@ -154,25 +168,38 @@ export function createContextCommandBarButtons(
|
||||
selectedNodeState: SelectedNodeState
|
||||
): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
|
||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||
const isPhoenixShellDisabled = !useNotebook.getState().isShellEnabled || useNotebook.getState().isPhoenixDisabled;
|
||||
const phoenixMongoShellBtn: CommandButtonComponentProps = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
iconAlt: "Open Mongo Shell",
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
commandButtonLabel: "Open Mongo Shell",
|
||||
ariaLabel: "Open Mongo Shell",
|
||||
hasPopup: true,
|
||||
disabled: isPhoenixShellDisabled,
|
||||
tooltipText: isPhoenixShellDisabled ? Constants.Notebook.notebookDisabledText : undefined,
|
||||
};
|
||||
buttons.push(newMongoShellBtn);
|
||||
buttons.push(phoenixMongoShellBtn);
|
||||
if (!useNotebook.getState().isShellEnabled) {
|
||||
const label = "New Shell";
|
||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: false,
|
||||
tooltipText: label,
|
||||
};
|
||||
buttons.push(newMongoShellBtn);
|
||||
}
|
||||
}
|
||||
|
||||
return buttons;
|
||||
@@ -401,7 +428,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
return buttons;
|
||||
}
|
||||
|
||||
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
|
||||
function applyNotebooksStyleProps(buttonProps: CommandButtonComponentProps, tooltip: string): void {
|
||||
if (!buttonProps.isDivider) {
|
||||
buttonProps.disabled = true;
|
||||
buttonProps.tooltipText = tooltip;
|
||||
@@ -477,10 +504,11 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
|
||||
|
||||
function createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
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 disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount &&
|
||||
(!useNotebook.getState().isNotebookEnabled ||
|
||||
!useNotebook.getState().isShellEnabled ||
|
||||
useNotebook.getState().isPhoenixDisabled);
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
@@ -493,7 +521,7 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
hasPopup: false,
|
||||
disabled: disableButton,
|
||||
ariaLabel: label,
|
||||
tooltipText: !disableButton ? "" : tooltip,
|
||||
tooltipText: !disableButton ? undefined : Constants.Notebook.notebookDisabledText,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@
|
||||
outline: 0px;
|
||||
}
|
||||
}
|
||||
.disableText{
|
||||
background-color: rgb(255, 255, 255)!important;
|
||||
filter: grayscale();
|
||||
color: rgb(161, 159, 157)!important;
|
||||
}
|
||||
.connectIcon{
|
||||
margin: 0px 4px;
|
||||
height: 18px;
|
||||
|
||||
@@ -23,6 +23,8 @@ interface Props {
|
||||
}
|
||||
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
|
||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||
const isPhoenixDisabled = useNotebook((state) => state.isPhoenixDisabled);
|
||||
|
||||
const [second, setSecond] = React.useState("00");
|
||||
const [minute, setMinute] = React.useState("00");
|
||||
const [isActive, setIsActive] = React.useState(false);
|
||||
@@ -77,6 +79,12 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
||||
}
|
||||
}, [connectionInfo.status]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isPhoenixDisabled) {
|
||||
setToolTipContent(Notebook.notebookDisabledText);
|
||||
}
|
||||
}, [isPhoenixDisabled]);
|
||||
|
||||
const stopTimer = () => {
|
||||
setIsActive(false);
|
||||
setCounter(0);
|
||||
@@ -93,11 +101,18 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
||||
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
|
||||
) {
|
||||
return (
|
||||
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
|
||||
<ActionButton
|
||||
className={isPhoenixDisabled ? "disableText commandReactBtn" : "commandReactBtn"}
|
||||
disabled={isPhoenixDisabled}
|
||||
onClick={() => !isPhoenixDisabled && container.allocateContainer()}
|
||||
>
|
||||
<TooltipHost content={toolTipContent}>
|
||||
<Stack className="connectionStatusContainer" horizontal>
|
||||
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
|
||||
<span>{connectionInfo.status}</span>
|
||||
<Icon
|
||||
iconName="ConnectVirtualMachine"
|
||||
className={isPhoenixDisabled ? "connectIcon disableText" : "connectIcon"}
|
||||
/>
|
||||
<span className={isPhoenixDisabled ? "disableText" : ""}>{connectionInfo.status}</span>
|
||||
</Stack>
|
||||
</TooltipHost>
|
||||
</ActionButton>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
@@ -154,6 +154,7 @@ export class NotebookContainerClient {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
poolId: PoolIdType.DefaultPoolId,
|
||||
};
|
||||
return await this.phoenixClient.resetContainer(provisionData);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Immutable from "immutable";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import loadTransform from "../NotebookComponent/loadTransform";
|
||||
@@ -100,6 +101,7 @@ export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaA
|
||||
// Only in cases where CosmosMongoKernel runs into an error we get a single output
|
||||
if (outputs.size === 1) {
|
||||
traceFailure(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
|
||||
Logger.logError(`Failed to analyze schema: ${JSON.stringify(data)}`, "SchemaAnalyzer/traceClickAnalyzeComplete");
|
||||
} else {
|
||||
traceSuccess(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType } from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo, ContainerInfo, PhoenixErrorType } from "../../Contracts/DataModels";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -40,6 +40,7 @@ interface NotebookState {
|
||||
containerStatus: ContainerInfo;
|
||||
isPhoenixNotebooks: boolean;
|
||||
isPhoenixFeatures: boolean;
|
||||
isPhoenixDisabled: boolean;
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||
@@ -64,6 +65,7 @@ interface NotebookState {
|
||||
getPhoenixStatus: () => Promise<void>;
|
||||
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
|
||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
|
||||
setIsPhoenixDisabled: (isPhoenixDisabled: boolean) => void;
|
||||
}
|
||||
|
||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
@@ -96,10 +98,11 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
containerStatus: {
|
||||
status: undefined,
|
||||
durationLeftInMinutes: undefined,
|
||||
notebookServerInfo: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
},
|
||||
isPhoenixNotebooks: undefined,
|
||||
isPhoenixFeatures: undefined,
|
||||
isPhoenixDisabled: undefined,
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||
@@ -296,26 +299,47 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
useNotebook.getState().setContainerStatus({
|
||||
status: undefined,
|
||||
durationLeftInMinutes: undefined,
|
||||
notebookServerInfo: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
});
|
||||
},
|
||||
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
||||
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
||||
getPhoenixStatus: async () => {
|
||||
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
|
||||
let isPhoenix = false;
|
||||
if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) {
|
||||
const phoenixClient = new PhoenixClient();
|
||||
isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted());
|
||||
}
|
||||
let isPhoenixNotebooks = false;
|
||||
let isPhoenixFeatures = false;
|
||||
let isPhoenixDisabled = false;
|
||||
|
||||
const isPhoenixNotebooks = userContext.features.phoenixNotebooks && isPhoenix;
|
||||
const isPhoenixFeatures = userContext.features.phoenixFeatures && isPhoenix;
|
||||
const isPublicInternetAllowed = isPublicInternetAccessAllowed();
|
||||
const phoenixClient = new PhoenixClient();
|
||||
const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus();
|
||||
|
||||
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
|
||||
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
|
||||
isPhoenixNotebooks = userContext.features.phoenixNotebooks;
|
||||
isPhoenixFeatures = userContext.features.phoenixFeatures;
|
||||
isPhoenixDisabled = !isPublicInternetAllowed && (isPhoenixNotebooks || isPhoenixFeatures);
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = true;
|
||||
isPhoenixDisabled = !isPublicInternetAllowed;
|
||||
}
|
||||
} else if (
|
||||
dbAccountAllowedInfo.status === HttpStatusCodes.Forbidden &&
|
||||
(userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures)
|
||||
) {
|
||||
isPhoenixNotebooks = userContext.features.phoenixNotebooks;
|
||||
isPhoenixFeatures = userContext.features.phoenixFeatures;
|
||||
isPhoenixDisabled = true;
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = false;
|
||||
}
|
||||
|
||||
set({ isPhoenixNotebooks: isPhoenixNotebooks });
|
||||
set({ isPhoenixFeatures: isPhoenixFeatures });
|
||||
set({ isPhoenixDisabled: isPhoenixDisabled });
|
||||
}
|
||||
},
|
||||
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
|
||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
|
||||
setIsPhoenixDisabled: (isPhoenixDisabled: boolean) => set({ isPhoenixDisabled: isPhoenixDisabled }),
|
||||
}));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -156,8 +156,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
label: "Gallery",
|
||||
iconSrc: GalleryIcon,
|
||||
className: "notebookHeader galleryHeader",
|
||||
toolTip: useNotebook.getState().isPhoenixDisabled ? Notebook.notebookDisabledText : undefined,
|
||||
onClick: () => container.openGallery(),
|
||||
isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery,
|
||||
isDisabled: useNotebook.getState().isPhoenixDisabled,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -523,6 +525,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
children.push({
|
||||
label: "Schema (Preview)",
|
||||
onClick: collection.onSchemaAnalyzerClick.bind(collection),
|
||||
toolTip: useNotebook.getState().isPhoenixDisabled ? Notebook.notebookDisabledText : undefined,
|
||||
isDisabled: useNotebook.getState().isPhoenixDisabled,
|
||||
isSelected: () =>
|
||||
useSelectedNode
|
||||
.getState()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { initializeIcons, Link, Text } from "@fluentui/react";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { userContext } from "UserContext";
|
||||
import { initializeConfiguration } from "../ConfigContext";
|
||||
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
||||
import {
|
||||
@@ -25,7 +26,9 @@ const onInit = async () => {
|
||||
|
||||
const props: GalleryAndNotebookViewerComponentProps = {
|
||||
junoClient: new JunoClient(),
|
||||
selectedTab: galleryViewerProps.selectedTab || GalleryTab.PublicGallery,
|
||||
selectedTab:
|
||||
galleryViewerProps.selectedTab ||
|
||||
(userContext.features.publicGallery ? GalleryTab.PublicGallery : GalleryTab.OfficialSamples),
|
||||
sortBy: galleryViewerProps.sortBy || SortBy.MostRecent,
|
||||
searchText: galleryViewerProps.searchText,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ContainerConnectionInfo,
|
||||
ContainerInfo,
|
||||
IContainerData,
|
||||
IDbAccountAllow,
|
||||
IMaxAllocationTimeExceeded,
|
||||
IMaxDbAccountsPerUserExceeded,
|
||||
IMaxUsersPerDbAccountExceeded,
|
||||
@@ -103,7 +104,7 @@ export class PhoenixClient {
|
||||
const containerStatus = await response.json();
|
||||
return {
|
||||
durationLeftInMinutes: containerStatus?.durationLeftInMinutes,
|
||||
notebookServerInfo: containerStatus?.notebookServerInfo,
|
||||
phoenixServerInfo: containerStatus?.phoenixServerInfo,
|
||||
status: ContainerStatusType.Active,
|
||||
};
|
||||
} else if (response.status === HttpStatusCodes.NotFound) {
|
||||
@@ -147,7 +148,7 @@ export class PhoenixClient {
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
return {
|
||||
durationLeftInMinutes: undefined,
|
||||
notebookServerInfo: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
status: ContainerStatusType.Disconnected,
|
||||
};
|
||||
}
|
||||
@@ -161,15 +162,17 @@ export class PhoenixClient {
|
||||
}
|
||||
}
|
||||
|
||||
public async isDbAcountWhitelisted(): Promise<boolean> {
|
||||
public async getDbAccountAllowedStatus(): Promise<IDbAccountAllow> {
|
||||
const startKey = TelemetryProcessor.traceStart(Action.PhoenixDBAccountAllowed, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
let responseJson;
|
||||
try {
|
||||
const response = await window.fetch(`${this.getPhoenixControlPlanePathPrefix()}`, {
|
||||
method: "GET",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
});
|
||||
responseJson = await response?.json();
|
||||
if (response.status !== HttpStatusCodes.OK) {
|
||||
throw new Error(`Received status code: ${response?.status}`);
|
||||
}
|
||||
@@ -180,7 +183,11 @@ export class PhoenixClient {
|
||||
},
|
||||
startKey
|
||||
);
|
||||
return response.status === HttpStatusCodes.OK;
|
||||
return {
|
||||
status: response.status,
|
||||
message: responseJson?.message,
|
||||
type: responseJson?.type,
|
||||
};
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.PhoenixDBAccountAllowed,
|
||||
@@ -192,7 +199,11 @@ export class PhoenixClient {
|
||||
startKey
|
||||
);
|
||||
Logger.logError(getErrorMessage(error), "PhoenixClient/IsDbAcountWhitelisted");
|
||||
return false;
|
||||
return {
|
||||
status: HttpStatusCodes.Forbidden,
|
||||
message: responseJson?.message,
|
||||
type: responseJson?.type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import SqlX from "./SqlX";
|
||||
import {
|
||||
FetchPricesResponse,
|
||||
PriceMapAndCurrencyCode,
|
||||
RegionItem,
|
||||
RegionsResponse,
|
||||
SqlxServiceResource,
|
||||
UpdateDedicatedGatewayRequestParameters,
|
||||
@@ -139,7 +140,7 @@ const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: str
|
||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
|
||||
};
|
||||
|
||||
export const getRegions = async (): Promise<Array<string>> => {
|
||||
export const getRegions = async (): Promise<Array<RegionItem>> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getRegions",
|
||||
@@ -149,8 +150,6 @@ export const getRegions = async (): Promise<Array<string>> => {
|
||||
const getRegionsTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
const regions = new Array<string>();
|
||||
|
||||
const response = await armRequestWithoutPolling<RegionsResponse>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
|
||||
@@ -158,20 +157,12 @@ export const getRegions = async (): Promise<Array<string>> => {
|
||||
apiVersion: "2021-04-01-preview",
|
||||
});
|
||||
|
||||
if (response.result.location !== undefined) {
|
||||
regions.push(response.result.location.split(" ").join("").toLowerCase());
|
||||
} else {
|
||||
for (const location of response.result.locations) {
|
||||
regions.push(location.locationName.split(" ").join("").toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getRegionsTimestamp);
|
||||
return regions;
|
||||
return response.result.properties.locations;
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
|
||||
return new Array<string>();
|
||||
return new Array<RegionItem>();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,7 +170,7 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
||||
};
|
||||
|
||||
export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promise<PriceMapAndCurrencyCode> => {
|
||||
export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Promise<PriceMapAndCurrencyCode> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getPriceMapAndCurrencyCode",
|
||||
@@ -191,7 +182,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
|
||||
try {
|
||||
const priceMap = new Map<string, Map<string, number>>();
|
||||
let currencyCode;
|
||||
for (const region of regions) {
|
||||
for (const regionItem of regions) {
|
||||
const regionPriceMap = new Map<string, number>();
|
||||
|
||||
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
||||
@@ -202,7 +193,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
|
||||
queryParams: {
|
||||
filter:
|
||||
"armRegionName eq '" +
|
||||
region +
|
||||
regionItem.locationName.split(" ").join("").toLowerCase() +
|
||||
"' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
|
||||
},
|
||||
});
|
||||
@@ -215,7 +206,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
|
||||
}
|
||||
regionPriceMap.set(item.skuName, item.retailPrice);
|
||||
}
|
||||
priceMap.set(region, regionPriceMap);
|
||||
priceMap.set(regionItem.locationName, regionPriceMap);
|
||||
}
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RegionItem } from "SelfServe/SqlX/SqlxTypes";
|
||||
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
|
||||
import {
|
||||
selfServeTrace,
|
||||
@@ -208,7 +209,7 @@ const ApproximateCostDropDownInfo: Info = {
|
||||
|
||||
let priceMap: Map<string, Map<string, number>>;
|
||||
let currencyCode: string;
|
||||
let regions: Array<string>;
|
||||
let regions: Array<RegionItem>;
|
||||
|
||||
const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
const telemetryData = {
|
||||
@@ -221,27 +222,47 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
|
||||
try {
|
||||
let costPerHour = 0;
|
||||
for (const region of regions) {
|
||||
const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", ""));
|
||||
let costBreakdown = "";
|
||||
for (const regionItem of regions) {
|
||||
const incrementalCost = priceMap.get(regionItem.locationName).get(skuName.replace("Cosmos.", ""));
|
||||
if (incrementalCost === undefined) {
|
||||
throw new Error("Value not found in map.");
|
||||
throw new Error(`${regionItem.locationName} not found in price map.`);
|
||||
} else if (incrementalCost === 0) {
|
||||
throw new Error(`${regionItem.locationName} cost per hour = 0`);
|
||||
}
|
||||
costPerHour += incrementalCost;
|
||||
|
||||
let regionalInstanceCount = instanceCount;
|
||||
if (regionItem.isZoneRedundant) {
|
||||
regionalInstanceCount = Math.ceil(instanceCount * 1.5);
|
||||
}
|
||||
|
||||
const regionalCostPerHour = incrementalCost * regionalInstanceCount;
|
||||
costBreakdown += `
|
||||
${regionItem.locationName} ${regionItem.isZoneRedundant ? "(AZ)" : ""}
|
||||
${regionalCostPerHour} ${currencyCode} (${regionalInstanceCount} instances * ${incrementalCost} ${currencyCode})\
|
||||
`;
|
||||
|
||||
if (regionalCostPerHour === 0) {
|
||||
throw new Error(`${regionItem.locationName} Cost per hour = 0`);
|
||||
}
|
||||
|
||||
costPerHour += regionalCostPerHour;
|
||||
}
|
||||
|
||||
if (costPerHour === 0) {
|
||||
throw new Error("Cost per hour = 0");
|
||||
}
|
||||
|
||||
costPerHour *= instanceCount;
|
||||
costPerHour = Math.round(costPerHour * 100) / 100;
|
||||
|
||||
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
|
||||
return {
|
||||
textTKey: `${costPerHour} ${currencyCode}`,
|
||||
textTKey: `${costPerHour} ${currencyCode}
|
||||
${costBreakdown}`,
|
||||
type: DescriptionType.Text,
|
||||
};
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
const failureTelemetry = { err, regions, priceMap, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, calculateCostTimestamp);
|
||||
|
||||
|
||||
@@ -48,10 +48,14 @@ export type PriceItem = {
|
||||
};
|
||||
|
||||
export type RegionsResponse = {
|
||||
properties: RegionsProperties;
|
||||
};
|
||||
|
||||
export type RegionsProperties = {
|
||||
locations: Array<RegionItem>;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type RegionItem = {
|
||||
locationName: string;
|
||||
isZoneRedundant: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user