Compare commits

..

1 Commits

Author SHA1 Message Date
sunilyadav840
4a227941b9 fixed tableEntity treeComponent and DefaultExperienceUtility file typescript issue 2021-08-24 19:38:19 +05:30
103 changed files with 1642 additions and 3377 deletions

View File

@@ -44,6 +44,8 @@ src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
src/Explorer/DataSamples/ContainerSampleGenerator.ts
src/Explorer/DataSamples/DataSamplesUtil.test.ts
src/Explorer/DataSamples/DataSamplesUtil.ts
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts
src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts

View File

@@ -1,8 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
<svg
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<path
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
transform="scale(0.5)" fill="#000" stroke="#000">
<path
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
transform="scale(0.5)"
fill="#000"
stroke="#CCC"
>
</path>
</svg>

Before

Width:  |  Height:  |  Size: 449 B

After

Width:  |  Height:  |  Size: 503 B

View File

@@ -37,8 +37,8 @@ module.exports = {
global: {
branches: 25,
functions: 25,
lines: 29.5,
statements: 29.5,
lines: 30,
statements: 30,
},
},

View File

@@ -2357,8 +2357,6 @@ a:link {
height: 100%;
flex-grow: 1;
overflow: hidden;
min-height: 300px;
overflow-y: scroll;
}
.tabs {
@@ -2834,8 +2832,6 @@ a:link {
#explorerNotificationConsole {
z-index: 1000;
overflow-y: auto;
overflow-x: clip;
}
.uniqueIndexesContainer {

View File

@@ -1,7 +1,6 @@
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
import React, { FunctionComponent } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import { userContext } from "../UserContext";
import { NormalizedEventKey } from "./Constants";
export interface CollapsedResourceTreeProps {
toggleLeftPaneExpanded: () => void;
@@ -12,21 +11,6 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
toggleLeftPaneExpanded,
isLeftPaneExpanded,
}: CollapsedResourceTreeProps): JSX.Element => {
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
useEffect(() => {
if (focusButton.current) {
focusButton.current.focus();
}
});
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
toggleLeftPaneExpanded();
event.stopPropagation();
}
};
return (
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
<div className="main-nav nav">
@@ -37,14 +21,11 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
role="button"
tabIndex={0}
aria-label="Expand Tree"
onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded}
ref={focusButton}
>
<span className="leftarrowCollapsed">
<span className="leftarrowCollapsed" onClick={toggleLeftPaneExpanded}>
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span>
<span className="collectionCollapsed">
<span className="collectionCollapsed" onClick={toggleLeftPaneExpanded}>
<span>{userContext.apiType} API</span>
</span>
</li>

View File

@@ -96,7 +96,6 @@ export class Flights {
public static readonly AutoscaleTest = "autoscaletest";
public static readonly PartitionKeyTest = "partitionkeytest";
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
public static readonly Phoenix = "phoenix";
}
export class AfecFeatures {
@@ -338,14 +337,6 @@ export enum ConflictOperationType {
Delete = "delete",
}
export enum ConnectionStatusType {
Connect = "Connect",
Connecting = "Connecting",
Connected = "Connected",
Failed = "Connection Failed",
ReConnect = "Reconnect",
}
export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
@@ -355,32 +346,15 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta
export class Notebook {
public static readonly defaultBasePath = "./notebooks";
public static readonly heartbeatDelayMs = 60000;
public static readonly heartbeatDelayMs = 5000;
public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000;
public static readonly memoryGuageToGB = 1048576;
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
public static readonly mongoShellTemporarilyDownMsg =
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
public static readonly cassandraShellTemporarilyDownMsg =
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
public static saveNotebookModalTitle = "Save Notebook in temporary workspace";
public static saveNotebookModalContent =
"This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends.";
public static newNotebookModalTitle = "Create Notebook in temporary workspace";
public static newNotebookUploadModalTitle = "Upload Notebook in temporary workspace";
public static newNotebookModalContent1 =
"A temporary workspace will be created to enable you to work with notebooks. When the session expires, any notebooks in the workspace will be removed.";
public static newNotebookModalContent2 =
"To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends. ";
public static galleryNotebookDownloadContent1 =
"To download, run, and make changes to this sample notebook, a temporary workspace will be created. When the session expires, any notebooks in the workspace will be removed.";
public static galleryNotebookDownloadContent2 =
"To save your work permanently, save your notebooks to a GitHub repository or download the Notebooks to your local machine before the session ends. ";
public static cosmosNotebookHomePageUrl = "https://aka.ms/cosmos-notebooks-limits";
public static cosmosNotebookGitDocumentationUrl = "https://aka.ms/cosmos-notebooks-github";
public static learnMore = "Learn more.";
}
export class SparkLibrary {

View File

@@ -3,16 +3,8 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels";
import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { updateUserContext } from "../UserContext";
import {
deleteDocument,
getEndpoint,
getFeatureEndpointOrDefault,
queryDocuments,
readDocument,
updateDocument,
} from "./MongoProxyClient";
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
const databaseId = "testDB";
@@ -254,31 +246,4 @@ describe("MongoProxyClient", () => {
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
});
});
describe("getFeatureEndpointOrDefault", () => {
beforeEach(() => {
resetConfigContext();
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
});
const params = new URLSearchParams({
"feature.mongoProxyEndpoint": "https://localhost:12901",
"feature.mongoProxyAPIs": "readDocument|createDocument",
});
const features = extractFeatures(params);
updateUserContext({
authType: AuthType.AAD,
features: features,
});
});
it("returns a local endpoint", () => {
const endpoint = getFeatureEndpointOrDefault("readDocument");
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
});
it("returns a production endpoint", () => {
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
});
});
});

View File

@@ -6,7 +6,6 @@ import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import { hasFlag } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
@@ -79,7 +78,7 @@ export function queryDocuments(
: "",
};
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
const endpoint = getEndpoint() || "";
const headers = {
...defaultHeaders,
@@ -142,8 +141,7 @@ export function readDocument(
: "",
};
const endpoint = getFeatureEndpointOrDefault("readDocument");
const endpoint = getEndpoint();
return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
method: "GET",
@@ -183,7 +181,7 @@ export function createDocument(
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
};
const endpoint = getFeatureEndpointOrDefault("createDocument");
const endpoint = getEndpoint();
return window
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
@@ -227,7 +225,7 @@ export function updateDocument(
? documentId.partitionKeyProperty
: "",
};
const endpoint = getFeatureEndpointOrDefault("updateDocument");
const endpoint = getEndpoint();
return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
@@ -268,7 +266,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
? documentId.partitionKeyProperty
: "",
};
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
const endpoint = getEndpoint();
return window
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
@@ -311,7 +309,7 @@ export function createMongoCollectionWithProxy(
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
};
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
const endpoint = getEndpoint();
return window
.fetch(
@@ -335,15 +333,8 @@ export function createMongoCollectionWithProxy(
});
}
export function getFeatureEndpointOrDefault(feature: string): string {
return hasFlag(userContext.features.mongoProxyAPIs, feature)
? getEndpoint(userContext.features.mongoProxyEndpoint)
: getEndpoint();
}
export function getEndpoint(customEndpoint?: string): string {
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
url += "/api/mongo/explorer";
export function getEndpoint(): string {
let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer";
if (userContext.authType === AuthType.EncryptedToken) {
url = url.replace("api/mongo", "api/guest/mongo");

View File

@@ -1,4 +1,4 @@
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
import React, { FunctionComponent } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import refreshImg from "../../images/refresh-cosmos.svg";
import { AuthType } from "../AuthType";
@@ -6,7 +6,6 @@ import Explorer from "../Explorer/Explorer";
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
import { userContext } from "../UserContext";
import { NormalizedEventKey } from "./Constants";
export interface ResourceTreeContainerProps {
toggleLeftPaneExpanded: () => void;
@@ -19,22 +18,6 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
isLeftPaneExpanded,
container,
}: ResourceTreeContainerProps): JSX.Element => {
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
useEffect(() => {
if (isLeftPaneExpanded) {
if (focusButton.current) {
focusButton.current.focus();
}
}
});
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
toggleLeftPaneExpanded();
event.stopPropagation();
}
};
return (
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
{/* Collections Window - - Start */}
@@ -60,11 +43,9 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
id="expandToggleLeftPaneButton"
role="button"
onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded}
tabIndex={0}
aria-label="Collapse Tree"
title="Collapse Tree"
ref={focusButton}
>
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
</span>

View File

@@ -36,7 +36,7 @@ export interface TableEntityProps {
onDeleteEntity?: () => void;
onEditEntity?: () => void;
onEntityPropertyChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onEntityTypeChange: (event: React.FormEvent<HTMLElement>, selectedParam: IDropdownOption) => void;
onEntityTypeChange: (event: React.FormEvent<HTMLElement>, selectedParam: IDropdownOption | undefined) => void;
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onSelectDate: (date: Date | null | undefined) => void;
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;

View File

@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
return (
<span>
<TooltipHost content={children}>
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" />
</TooltipHost>
</span>
);

View File

@@ -1,5 +1,3 @@
import { ConnectionStatusType } from "../Common/Constants";
export interface DatabaseAccount {
id: string;
name: string;
@@ -498,8 +496,3 @@ export interface MemoryUsageInfo {
freeKB: number;
totalKB: number;
}
export interface ContainerConnectionInfo {
status: ConnectionStatusType;
//need to add ram and rom info
}

View File

@@ -1,6 +1,5 @@
import { Icon, Label, Stack } from "@fluentui/react";
import * as React from "react";
import { NormalizedEventKey } from "../../../Common/Constants";
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
export interface CollapsibleSectionProps {
@@ -31,13 +30,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
}
}
private onKeyPress = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
this.toggleCollapsed();
event.stopPropagation();
}
};
public render(): JSX.Element {
return (
<>
@@ -47,11 +39,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
verticalAlign="center"
tokens={accordionStackTokens}
onClick={this.toggleCollapsed}
onKeyPress={this.onKeyPress}
tabIndex={0}
aria-name="Advanced"
role="button"
aria-expanded={this.state.isExpanded}
>
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
<Label>{this.props.title}</Label>

View File

@@ -3,14 +3,9 @@
exports[`CollapsibleSectionComponent renders 1`] = `
<Fragment>
<Stack
aria-expanded={true}
aria-name="Advanced"
className="collapsibleSection"
horizontal={true}
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
tokens={
Object {
"childrenGap": 10,

View File

@@ -13,7 +13,6 @@ import {
Link,
PrimaryButton,
ProgressIndicator,
Text,
TextField,
} from "@fluentui/react";
import React, { FC } from "react";
@@ -31,7 +30,6 @@ export interface DialogState {
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
contentHtml?: JSX.Element,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
@@ -60,7 +58,6 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
contentHtml?: JSX.Element,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
@@ -79,7 +76,6 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
get().closeDialog();
onCancel && onCancel();
},
contentHtml,
choiceGroupProps,
textFieldProps,
primaryButtonDisabled,
@@ -128,7 +124,6 @@ export interface DialogProps {
type?: DialogType;
showCloseButton?: boolean;
onDismiss?: () => void;
contentHtml?: JSX.Element;
}
const DIALOG_MIN_WIDTH = "400px";
@@ -155,7 +150,6 @@ export const Dialog: FC = () => {
type,
showCloseButton,
onDismiss,
contentHtml,
} = props || {};
const dialogProps: IDialogProps = {
@@ -187,7 +181,8 @@ export const Dialog: FC = () => {
text: secondaryButtonText,
onClick: onSecondaryButtonClick,
}
: undefined;
: {};
return visible ? (
<FluentDialog {...dialogProps}>
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
@@ -197,7 +192,6 @@ export const Dialog: FC = () => {
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link>
)}
{contentHtml && <Text>{contentHtml}</Text>}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter>
<PrimaryButton {...primaryButtonProps} />

View File

@@ -8,9 +8,6 @@
.input-type-head-text-field {
width: 100%;
}
.input-query-form {
width: 100%;
}
textarea {
width: 100%;
line-height: 1;

View File

@@ -160,21 +160,18 @@ export class InputTypeaheadComponent extends React.Component<
return (
<div className="input-typeahead-container">
<Stack horizontal>
<form aria-labelledby="input" className="input-query-form">
<TextField
multiline={useTextarea}
rows={1}
id="input"
defaultValue={defaultValue}
ariaLabel="Input query"
placeholder={placeholder}
className="input-type-head-text-field"
value={defaultValue}
onKeyDown={this.onSubmit}
onFocus={() => this.setState({ isSuggestionVisible: true })}
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
/>
</form>
<TextField
multiline={useTextarea}
rows={1}
defaultValue={defaultValue}
ariaLabel="Input query"
placeholder={placeholder}
className="input-type-head-text-field"
value={defaultValue}
onKeyDown={this.onSubmit}
onFocus={() => this.setState({ isSuggestionVisible: true })}
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
/>
{this.props.showCancelButton && (
<IconButton
styles={iconButtonStyles}

View File

@@ -7,22 +7,16 @@ exports[`inputTypeahead renders <input /> 1`] = `
<Stack
horizontal={true}
>
<form
aria-labelledby="input"
className="input-query-form"
>
<StyledTextFieldBase
ariaLabel="Input query"
className="input-type-head-text-field"
id="input"
multiline={false}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="placeholder"
rows={1}
/>
</form>
<StyledTextFieldBase
ariaLabel="Input query"
className="input-type-head-text-field"
multiline={false}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="placeholder"
rows={1}
/>
</Stack>
</div>
`;
@@ -34,22 +28,16 @@ exports[`inputTypeahead renders <textarea /> 1`] = `
<Stack
horizontal={true}
>
<form
aria-labelledby="input"
className="input-query-form"
>
<StyledTextFieldBase
ariaLabel="Input query"
className="input-type-head-text-field"
id="input"
multiline={true}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="placeholder"
rows={1}
/>
</form>
<StyledTextFieldBase
ariaLabel="Input query"
className="input-type-head-text-field"
multiline={true}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="placeholder"
rows={1}
/>
</Stack>
</div>
`;

View File

@@ -17,8 +17,6 @@ import Explorer from "../../Explorer";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import { useNotebook } from "../../Notebook/useNotebook";
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less";
@@ -148,9 +146,7 @@ export class NotebookViewerComponent
<NotebookMetadataComponent
data={this.state.galleryItem}
isFavorite={this.state.isFavorite}
downloadButtonText={
this.props.container && NotebookUtil.getNotebookBtnTitle(useNotebook.getState().notebookFolderName)
}
downloadButtonText={this.props.container && "Download to my notebooks"}
onTagClick={this.props.onTagClick}
onFavoriteClick={this.favoriteItem}
onUnfavoriteClick={this.unfavoriteItem}

View File

@@ -1,45 +1,45 @@
import * as React from "react";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import { Urls, StyleConstants } from "../../../Common/Constants";
import {
getPriceCurrency,
getCurrencySign,
getAutoscalePricePerRu,
getMultimasterMultiplier,
computeRUUsagePriceHourly,
getPricePerRu,
estimatedCostDisclaimer,
} from "../../../Utils/PricingUtils";
import {
DetailsList,
DetailsListLayoutMode,
DetailsRow,
ICheckboxStyles,
IChoiceGroupStyles,
IColumn,
IDetailsColumnStyles,
IDetailsListStyles,
IDetailsRowProps,
IDetailsRowStyles,
IDropdownStyles,
IMessageBarStyles,
ISeparatorStyles,
IStackProps,
IStackStyles,
IStackTokens,
ITextFieldStyles,
ITextStyles,
ICheckboxStyles,
IStackProps,
IStackTokens,
IChoiceGroupStyles,
Link,
Text,
IMessageBarStyles,
ITextStyles,
IDetailsRowStyles,
IStackStyles,
IDetailsListStyles,
IDropdownStyles,
ISeparatorStyles,
MessageBar,
MessageBarType,
SelectionMode,
Stack,
Spinner,
SpinnerSize,
Stack,
Text,
DetailsList,
IColumn,
SelectionMode,
DetailsListLayoutMode,
IDetailsRowProps,
DetailsRow,
IDetailsColumnStyles,
} from "@fluentui/react";
import * as React from "react";
import { StyleConstants, Urls } from "../../../Common/Constants";
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import {
computeRUUsagePriceHourly,
estimatedCostDisclaimer,
getAutoscalePricePerRu,
getCurrencySign,
getMultimasterMultiplier,
getPriceCurrency,
getPricePerRu,
} from "../../../Utils/PricingUtils";
import { isDirty, isDirtyTypes } from "./SettingsUtils";
import { isDirtyTypes, isDirty } from "./SettingsUtils";
export interface EstimatedSpendingDisplayProps {
costType: JSX.Element;
@@ -65,7 +65,7 @@ export interface PriceBreakdown {
currencySign: string;
}
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
label: {
@@ -223,15 +223,14 @@ export const getRuPriceBreakdown = (
multimasterEnabled: isMultimaster,
isAutoscale: isAutoscale,
});
const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster);
const pricePerRu: number = isAutoscale
? getAutoscalePricePerRu(serverId, multimasterMultiplier)
: getPricePerRu(serverId, multimasterMultiplier);
const basePricePerRu: number = isAutoscale
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
: getPricePerRu(serverId);
return {
hourlyPrice,
hourlyPrice: hourlyPrice,
dailyPrice: hourlyPrice * 24,
monthlyPrice: hourlyPrice * hoursInAMonth,
pricePerRu,
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
currency: getPriceCurrency(serverId),
currencySign: getCurrencySign(serverId),
};
@@ -272,7 +271,7 @@ export const manualToAutoscaleDisclaimerElement: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
<Link href={Urls.autoscaleMigration}>Learn more</Link>
<a href={Urls.autoscaleMigration}>Learn more</a>
</Text>
);

View File

@@ -8,7 +8,6 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}

View File

@@ -20,7 +20,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -40,7 +39,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -75,7 +73,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -83,11 +80,11 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
>
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
<StyledLinkBase
<a
href="https://aka.ms/cosmos-autoscale-migration"
>
Learn more
</StyledLinkBase>
</a>
</Text>
</StyledMessageBar>
<StyledChoiceGroup
@@ -189,7 +186,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -464,7 +460,6 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}

View File

@@ -16,7 +16,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}

View File

@@ -136,7 +136,6 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -413,7 +412,6 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -954,7 +952,6 @@ exports[`SubSettingsComponent renders 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -1231,7 +1228,6 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}

View File

@@ -34,7 +34,6 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -102,7 +101,6 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],

View File

@@ -159,7 +159,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -167,17 +166,16 @@ exports[`SettingsUtils functions render 1`] = `
>
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
<StyledLinkBase
<a
href="https://aka.ms/cosmos-autoscale-migration"
>
Learn more
</StyledLinkBase>
</a>
</Text>
<Text
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -197,7 +195,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -210,7 +207,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -223,7 +219,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -235,7 +230,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -255,7 +249,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -271,7 +264,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -286,7 +278,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -300,7 +291,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -312,7 +302,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -332,7 +321,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -375,7 +363,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -391,7 +378,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
@@ -408,7 +394,6 @@ exports[`SettingsUtils functions render 1`] = `
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}

View File

@@ -1,6 +1,7 @@
@import "../../../../less/Common/Constants";
.tabComponentContainer {
overflow: hidden;
height: 100%;
.flex-display();
.flex-direction();

View File

@@ -6,7 +6,6 @@ import { userContext } from "../../../../UserContext";
import {
calculateEstimateNumber,
computeRUUsagePriceHourly,
estimatedCostDisclaimer,
getAutoscalePricePerRu,
getCurrencySign,
getMultimasterMultiplier,
@@ -43,9 +42,11 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId);
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
const pricePerRu = isAutoscale
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
: getPricePerRu(serverId) * multiplier;
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>;
if (isAutoscale) {
return (

View File

@@ -118,7 +118,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<input
className="throughputInputRadioBtn"
aria-label="Autoscale mode"
aria-required={true}
checked={isAutoscaleSelected}
type="radio"
role="radio"
@@ -132,7 +131,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
aria-label="Manual mode"
checked={!isAutoscaleSelected}
type="radio"
aria-required={true}
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")}

View File

@@ -345,14 +345,12 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
ariaLabel="Info"
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
>
<IconBase
ariaLabel="Info"
className="panelInfoIcon"
iconName="Info"
styles={[Function]}
tabIndex={0}
theme={
Object {
"disableGlobalClassNames": false,
@@ -632,7 +630,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
className="panelInfoIcon root-57"
data-icon-name="Info"
role="img"
tabIndex={0}
>
</i>
@@ -654,7 +651,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
>
<input
aria-label="Autoscale mode"
aria-required={true}
checked={true}
className="throughputInputRadioBtn"
key=".0:$.0"
@@ -671,7 +667,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
</span>
<input
aria-label="Manual mode"
aria-required={true}
checked={false}
className="throughputInputRadioBtn"
key=".0:$.2"
@@ -1332,14 +1327,12 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
ariaLabel="Info"
className="panelInfoIcon"
iconName="Info"
tabIndex={0}
>
<IconBase
ariaLabel="Info"
className="panelInfoIcon"
iconName="Info"
styles={[Function]}
tabIndex={0}
theme={
Object {
"disableGlobalClassNames": false,
@@ -1619,7 +1612,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
className="panelInfoIcon root-57"
data-icon-name="Info"
role="img"
tabIndex={0}
>
</i>

View File

@@ -43,7 +43,7 @@ export interface TreeNode {
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean;
isSelected?: () => boolean;
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
onClick?: (isExpanded?: boolean) => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => void;
onCollapsed?: () => void;
onContextMenuOpen?: () => void;
@@ -73,7 +73,7 @@ interface TreeNodeComponentProps {
}
interface TreeNodeComponentState {
isExpanded: boolean;
isExpanded?: boolean;
isMenuShowing: boolean;
}
export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, TreeNodeComponentState> {
@@ -82,7 +82,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
private static readonly transitionDurationMS = 200;
private static readonly callbackDelayMS = 100; // avoid calling at the same time as transition to make it smoother
private contextMenuRef = React.createRef<HTMLDivElement>();
private isExpanded: boolean;
private isExpanded?: boolean;
constructor(props: TreeNodeComponentProps) {
super(props);
@@ -93,7 +93,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
};
}
componentDidUpdate(prevProps: TreeNodeComponentProps, prevState: TreeNodeComponentState) {
componentDidUpdate(_prevProps: TreeNodeComponentProps, prevState: TreeNodeComponentState) {
// Only call when expand has actually changed
if (this.state.isExpanded !== prevState.isExpanded) {
if (this.state.isExpanded) {
@@ -103,7 +103,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
}
}
if (this.props.node.isExpanded !== this.isExpanded) {
this.isExpanded = this.props.node.isExpanded;
this.isExpanded = this.props.node && this.props.node.isExpanded;
this.setState({
isExpanded: this.props.node.isExpanded,
});
@@ -114,7 +114,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
return this.renderNode(this.props.node, this.props.generation);
}
private static getSortedChildren(treeNode: TreeNode): TreeNode[] {
private static getSortedChildren(treeNode: TreeNode): TreeNode[] | undefined {
if (!treeNode || !treeNode.children) {
return undefined;
}
@@ -195,7 +195,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
{node.children && (
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
<div className="nodeChildren" data-test={node.label}>
{TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => (
{TreeNodeComponent?.getSortedChildren(node)?.map((childNode: TreeNode) => (
<TreeNodeComponent
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}
node={childNode}
@@ -214,15 +214,15 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
* Recursive: is the node or any descendant selected
* @param node
*/
private static isAnyDescendantSelected(node: TreeNode): boolean {
return (
node.children &&
node.children.reduce(
(previous: boolean, child: TreeNode) =>
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child),
false
)
);
return node.children
? node.children.reduce(
(previous: boolean, child: TreeNode) =>
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child),
false
)
: false;
}
private static createClickEvent(): MouseEvent {
@@ -230,7 +230,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
}
private onRightClick = (): void => {
this.contextMenuRef.current.firstChild.dispatchEvent(TreeNodeComponent.createClickEvent());
this.contextMenuRef?.current?.firstChild?.dispatchEvent(TreeNodeComponent.createClickEvent());
};
private renderContextMenuButton(node: TreeNode): JSX.Element {
@@ -243,7 +243,6 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
<IconButton
name="More"
title="More"
className="treeMenuEllipsis"
ariaLabel={menuItemLabel}
menuIconProps={{
@@ -254,18 +253,18 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
coverTarget: true,
isBeakVisible: false,
directionalHint: DirectionalHint.topAutoEdge,
onMenuOpened: (contextualMenu?: IContextualMenuProps) => {
onMenuOpened: (_contextualMenu?: IContextualMenuProps) => {
this.setState({ isMenuShowing: true });
node.onContextMenuOpen && node.onContextMenuOpen();
},
onMenuDismissed: (contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }),
onMenuDismissed: (_contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }),
contextualMenuItemAs: (props: IContextualMenuItemProps) => (
<div
data-test={`treeComponentMenuItemContainer`}
className="treeComponentMenuItemContainer"
onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
>
{props.item.onRenderIcon()}
{props.item.onRenderIcon && props.item.onRenderIcon()}
<span
className={
"treeComponentMenuItemLabel" + (props.item.className ? ` ${props.item.className}Label` : "")
@@ -275,19 +274,21 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
</span>
</div>
),
items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
key: menuItem.label,
text: menuItem.label,
disabled: menuItem.isDisabled,
className: menuItem.styleClass,
onClick: () => {
menuItem.onClick();
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
label: menuItem.label,
});
},
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />,
})),
items: node.contextMenu
? node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
key: menuItem.label,
text: menuItem.label,
disabled: menuItem.isDisabled,
className: menuItem.styleClass,
onClick: () => {
menuItem.onClick();
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
label: menuItem.label,
});
},
onRenderIcon: (_props: any) => <img src={menuItem.iconSrc} alt="" />,
}))
: [],
}}
styles={buttonStyles}
/>
@@ -325,7 +326,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);
};
private onNodeKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: TreeNode): void => {
private onNodeKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, _node: TreeNode): void => {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation();
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);

View File

@@ -211,7 +211,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
},
}
}
title="More"
/>
</div>
</div>
@@ -424,7 +423,6 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
},
}
}
title="More"
/>
</div>
</div>

View File

@@ -1,11 +1,9 @@
import { Link } from "@fluentui/react/lib/Link";
import * as ko from "knockout";
import React from "react";
import _ from "underscore";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
import { ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
@@ -13,13 +11,11 @@ import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHand
import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient";
import * as DataModels from "../Contracts/DataModels";
import { ContainerConnectionInfo } from "../Contracts/DataModels";
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";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
@@ -91,13 +87,12 @@ export default class Explorer {
};
private static readonly MaxNbDatabasesToAutoExpand = 5;
private phoenixClient: PhoenixClient;
constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree,
});
this._isInitializingNotebooks = false;
this.phoenixClient = new PhoenixClient();
useNotebook.subscribe(
() => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount
@@ -166,10 +161,23 @@ export default class Explorer {
useNotebook.subscribe(
async () => {
this.initiateAndRefreshNotebookList();
useNotebook.getState().setIsRefreshed(false);
if (!this.notebookManager) {
const NotebookManager = await (
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
).default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
},
(state) => state.isNotebookEnabled || state.isRefreshed
(state) => state.isNotebookEnabled
);
this.resourceTree = new ResourceTreeAdapter(this);
@@ -202,23 +210,6 @@ export default class Explorer {
this.refreshExplorer();
}
public async initiateAndRefreshNotebookList(): Promise<void> {
if (!this.notebookManager) {
const NotebookManager = (await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"))
.default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
}
public openEnableSynapseLinkDialog(): void {
const addSynapseLinkDialogProps: DialogProps = {
linkProps: {
@@ -352,75 +343,28 @@ export default class Explorer {
return;
}
this._isInitializingNotebooks = true;
if (userContext.features.phoenix === false) {
await this.ensureNotebookWorkspaceRunning();
const connectionInfo = await listConnectionInfo(
userContext.subscriptionId,
userContext.resourceGroup,
databaseAccount.name,
"default"
);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
});
}
await this.ensureNotebookWorkspaceRunning();
const connectionInfo = await listConnectionInfo(
userContext.subscriptionId,
userContext.resourceGroup,
databaseAccount.name,
"default"
);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
});
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
public async allocateContainer(): Promise<void> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const isAllocating = useNotebook.getState().isAllocating;
if (isAllocating === false && notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined) {
const provisionData = {
aadToken: userContext.authorizationToken,
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
};
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
try {
useNotebook.getState().setIsAllocating(true);
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
if (
connectionInfo.status === HttpStatusCodes.OK &&
connectionInfo.data &&
connectionInfo.data.notebookServerUrl
) {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
});
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
useNotebook.getState().setIsAllocating(false);
} else {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetConatinerConnection(connectionStatus);
}
} catch (error) {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetConatinerConnection(connectionStatus);
throw error;
}
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
}
public resetNotebookWorkspace(): void {
public resetNotebookWorkspace() {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError(
"Attempt to reset notebook workspace, but notebook is not enabled",
@@ -445,6 +389,7 @@ export default class Explorer {
if (!databaseAccount) {
return false;
}
try {
const { value: workspaces } = await listByDatabaseAccount(
userContext.subscriptionId,
@@ -691,9 +636,6 @@ export default class Explorer {
if (!notebookContentItem || !notebookContentItem.path) {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
}
if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) {
this.allocateContainer();
}
const notebookTabs = useTabs
.getState()
@@ -915,51 +857,9 @@ export default class Explorer {
handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error);
}
const isPhoenixEnabled = NotebookUtil.isPhoenixEnabled();
if (isPhoenixEnabled) {
if (isGithubTree) {
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
};
} else {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookModalTitle,
undefined,
"Create",
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
},
"Cancel",
undefined,
this.getNewNoteWarningText()
);
}
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
}
}
private getNewNoteWarningText(): JSX.Element {
return (
<>
<p>{Notebook.newNotebookModalContent1}</p>
<br />
<p>
{Notebook.newNotebookModalContent2}
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
parent = parent || this.resourceTree.myNotebooksContentRoot;
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook,
@@ -1006,26 +906,7 @@ export default class Explorer {
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
}
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (NotebookUtil.isPhoenixEnabled()) {
await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
this.connectToNotebookTerminal(kind);
} else {
useDialog
.getState()
.showOkModalDialog(
"Failed to Connect",
"Failed to connect temporary workspace, this could happen because of network issue please refresh and try again."
);
}
} else {
this.connectToNotebookTerminal(kind);
}
}
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
let title: string;
switch (kind) {
@@ -1076,7 +957,7 @@ export default class Explorer {
notebookUrl?: string,
galleryItem?: IGalleryItem,
isFavorite?: boolean
): Promise<void> {
) {
const title = "Gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
const galleryTab = useTabs
@@ -1145,10 +1026,7 @@ export default class Explorer {
}
public async handleOpenFileAction(path: string): Promise<void> {
if (
userContext.features.phoenix === false &&
!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))
) {
if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
this._openSetupNotebooksPaneForQuickstart();
}
@@ -1180,27 +1058,7 @@ export default class Explorer {
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
if (NotebookUtil.isPhoenixEnabled()) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
"Upload",
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
},
"Cancel",
undefined,
this.getNewNoteWarningText()
);
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
}
}
private uploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot;
useSidePanel
.getState()
.openSidePanel(
@@ -1209,36 +1067,15 @@ export default class Explorer {
);
}
public getDownloadModalConent(fileName: string): JSX.Element {
if (NotebookUtil.isPhoenixEnabled()) {
return (
<>
<p>{Notebook.galleryNotebookDownloadContent1}</p>
<br />
<p>
{Notebook.galleryNotebookDownloadContent2}
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
}
public async refreshExplorer(): Promise<void> {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
let isNotebookEnabled = true;
if (!userContext.features.phoenix) {
isNotebookEnabled =
userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
userContext.features.enableNotebooks);
}
const isNotebookEnabled: boolean =
userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
userContext.features.enableNotebooks);
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());

View File

@@ -6,7 +6,7 @@ describe("Cache arrays by key", () => {
const key = "key";
cache.insert(key, 1, 0);
cache.clear();
expect(cache.retrieve(key, 0, 1)).toBe(undefined);
expect(cache.retrieve(key, 0, 1)).toBe(null);
});
it("should invalidate oldest key to keep cache size under maximum", () => {
@@ -21,7 +21,7 @@ describe("Cache arrays by key", () => {
cache.insert(key1, 2, 4);
expect(cache.retrieve(key1, 0, 3)).toEqual([0, 2, 4]);
expect(cache.retrieve(key2, 1, 1)).toEqual(undefined);
expect(cache.retrieve(key2, 1, 1)).toEqual(null);
});
it("should cache and retrieve cached page within boundaries", () => {
@@ -39,7 +39,7 @@ describe("Cache arrays by key", () => {
const key = "key";
cache.insert(key, 0, 0);
cache.insert(key, 1, 1);
expect(cache.retrieve(key, 2, 1)).toEqual(undefined);
expect(cache.retrieve(key, 2, 1)).toEqual(null);
});
it("should not retrieve cached page overlapping boundaries", () => {
@@ -48,7 +48,7 @@ describe("Cache arrays by key", () => {
cache.insert(key, 0, 0);
cache.insert(key, 1, 1);
cache.insert(key, 2, 2);
expect(cache.retrieve(key, 2, 4)).toEqual(undefined);
expect(cache.retrieve(key, 2, 4)).toEqual(null);
});
it("should not insert non-contiguous element", () => {
@@ -57,7 +57,7 @@ describe("Cache arrays by key", () => {
cache.insert(key, 0, 0);
cache.insert(key, 1, 1);
cache.insert(key, 3, 3);
expect(cache.retrieve(key, 3, 1)).toEqual(undefined);
expect(cache.retrieve(key, 3, 1)).toEqual(null);
});
it("should cache multiple keys", () => {

View File

@@ -61,12 +61,12 @@ export class ArraysByKeyCache<T> {
* @param pageSize
*/
public retrieve(key: string, startIndex: number, pageSize: number): T[] | null {
if (!Object.prototype.hasOwnProperty.call(this.cache, key)) {
return undefined;
if (!this.cache.hasOwnProperty(key)) {
return null;
}
const elements = this.cache[key];
if (startIndex + pageSize > elements.length) {
return undefined;
return null;
}
return elements.slice(startIndex, startIndex + pageSize);

View File

@@ -1,8 +1,8 @@
import * as React from "react";
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
import CollapseArrowIcon from "../../../../images/Collapse_arrow_14x14.svg";
import ExpandIcon from "../../../../images/Expand_14x14.svg";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
interface MiddlePaneComponentProps {
isTabsContentExpanded: boolean;
@@ -17,14 +17,7 @@ export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProp
<div className="middlePane">
<div className="graphTitle">
<span className="paneTitle">Graph</span>
<span
className="graphExpandCollapseBtn pull-right"
onClick={this.props.toggleExpandGraph}
role="button"
aria-expanded={this.props.isTabsContentExpanded}
aria-name="View graph in full screen"
tabIndex={0}
>
<span className="graphExpandCollapseBtn pull-right" onClick={this.props.toggleExpandGraph}>
<img
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,8 @@ import create, { UseStore } from "zustand";
import { StyleConstants } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import { userContext } from "../../../UserContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
@@ -56,15 +54,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (NotebookUtil.isPhoenixEnabled()) {
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
}
if (
userContext.features.phoenix === false &&
userContext.features.notebooksTemporarilyDown === false &&
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
) {
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
}

View File

@@ -80,9 +80,8 @@ export function createStaticCommandBarButtons(
}
notebookButtons.push(createOpenTerminalButton(container));
if (userContext.features.phoenix === false) {
notebookButtons.push(createNotebookWorkspaceResetButton(container));
}
notebookButtons.push(createNotebookWorkspaceResetButton(container));
if (
(userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled &&
@@ -596,7 +595,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
return {
iconSrc: GitHubIcon,
iconAlt: label,
onCommandClick: () => {
onCommandClick: () =>
useSidePanel
.getState()
.openSidePanel(
@@ -606,8 +605,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={junoClient}
/>
);
},
),
commandButtonLabel: label,
hasPopup: false,
disabled: false,

View File

@@ -13,8 +13,6 @@ import { StyleConstants } from "../../../Common/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { ConnectionStatus } from "./ConnectionStatusComponent";
import { MemoryTracker } from "./MemoryTrackerComponent";
/**
@@ -203,10 +201,3 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
onRender: () => <MemoryTracker />,
};
};
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <ConnectionStatus container={container} />,
};
};

View File

@@ -1,184 +0,0 @@
@import "../../../../less/Common/Constants";
.connectionStatusContainer {
cursor: default;
align-items: center;
border: 1px;
min-height: 44px;
> span {
padding-right: 12px;
font-size: 12px;
font-family: @DataExplorerFont;
color: @DefaultFontColor;
}
&:focus{
outline: 0px;
}
}
.commandReactBtn {
&:hover {
background-color: rgb(238, 247, 255);
color: rgb(32, 31, 30);
cursor: pointer;
}
&:focus{
outline: 1px dashed #605e5c;
}
}
.connectedReactBtn {
&:hover {
background-color: rgb(238, 247, 255);
color: rgb(32, 31, 30);
cursor: pointer;
}
&:focus{
outline: 0px;
}
}
.connectIcon{
margin: 0px 4px;
height: 18px;
width: 18px;
color: rgb(0, 120, 212);
}
.status {
position: relative;
display: block;
margin-right: 8px;
width: 1em;
height: 1em;
font-size: 9px!important;
padding: 0px!important;
border-radius: 0.5em;
}
.status::before,
.status::after {
position: absolute;
content: "";
}
.status::before {
top: 0;
left: 0;
width: 1em;
height: 1em;
background-color: rgba(#fff, 0.1);
border-radius: 100%;
opacity: 1;
transform: translate3d(0, 0, 0) scale(0);
}
.connected{
background-color: green;
box-shadow:
0 0 0 0em rgba(green, 0),
0em 0.05em 0.1em rgba(#000000, 0.2);
transform: translate3d(0, 0, 0) scale(1);
}
.connecting{
background-color:#ffbf00;
box-shadow:
0 0 0 0em rgba(#ffbf00, 0),
0em 0.05em 0.1em rgba(#000000, 0.2);
transform: translate3d(0, 0, 0) scale(1);
}
.failed{
background-color:#bd1919;
box-shadow:
0 0 0 0em rgba(#bd1919, 0),
0em 0.05em 0.1em rgba(#000000, 0.2);
transform: translate3d(0, 0, 0) scale(1);
}
.status.connecting.is-animating {
animation: status-outer-connecting 3000ms infinite;
}
.status.failed.is-animating {
animation: status-outer-failed 3000ms infinite;
}
.status.connected.is-animating {
animation: status-outer-connected 3000ms infinite;
}
@keyframes status-outer-connected {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #008000, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.6), 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@keyframes status-outer-failed {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #bd1919, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #c52d2d, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #b47b7b, 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@keyframes status-outer-connecting {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #ffbf00, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #f0dfad, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(198, 243, 198, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(213, 241, 213, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}

View File

@@ -1,107 +0,0 @@
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
import { ActionButton } from "@fluentui/react/lib/Button";
import * as React from "react";
import "../../../../less/hostedexplorer.less";
import { ConnectionStatusType, Notebook } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less";
interface Props {
container: Explorer;
}
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00");
const [isActive, setIsActive] = React.useState(false);
const [counter, setCounter] = React.useState(0);
const [statusColor, setStatusColor] = React.useState("");
const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
React.useEffect(() => {
let intervalId: NodeJS.Timeout;
if (isActive) {
intervalId = setInterval(() => {
const secondCounter = counter % 60;
const minuteCounter = Math.floor(counter / 60);
const computedSecond: string = String(secondCounter).length === 1 ? `0${secondCounter}` : `${secondCounter}`;
const computedMinute: string = String(minuteCounter).length === 1 ? `0${minuteCounter}` : `${minuteCounter}`;
setSecond(computedSecond);
setMinute(computedMinute);
setCounter((counter) => counter + 1);
}, 1000);
}
return () => clearInterval(intervalId);
}, [isActive, counter]);
const stopTimer = () => {
setIsActive(false);
setCounter(0);
setSecond("00");
setMinute("00");
};
const connectionInfo = useNotebook((state) => state.connectionInfo);
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
if (
connectionInfo &&
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.ReConnect)
) {
return (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
<span>{connectionInfo.status}</span>
</Stack>
</TooltipHost>
</ActionButton>
);
}
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
setIsActive(true);
setStatusColor("status connecting is-animating");
setToolTipContent("Connecting to temporary workspace.");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
stopTimer();
setStatusColor("status connected is-animating");
setToolTipContent("Connected to temporary workspace.");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
stopTimer();
setStatusColor("status failed is-animating");
setToolTipContent("Click here to Reconnect to temporary workspace.");
}
return (
<ActionButton
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
}
>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<i className={statusColor}></i>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
)}
</Stack>
</TooltipHost>
</ActionButton>
);
};

View File

@@ -129,7 +129,7 @@ export class NotificationConsoleComponent extends React.Component<
className="expandCollapseButton"
role="button"
tabIndex={0}
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
aria-label={"console button" + (this.props.isConsoleExpanded ? " collapsed" : " expanded")}
aria-expanded={!this.props.isConsoleExpanded}
>
<img
@@ -205,9 +205,7 @@ export class NotificationConsoleComponent extends React.Component<
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
<span className="date">{item.date}</span>
<span className="message" role="alert" aria-live="assertive">
{item.message}
</span>
<span className="message">{item.message}</span>
</div>
));
}

View File

@@ -70,7 +70,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
</div>
<div
aria-expanded={true}
aria-label="console button collapsed"
aria-label="console button expanded"
className="expandCollapseButton"
role="button"
tabIndex={0}
@@ -236,7 +236,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
</div>
<div
aria-expanded={true}
aria-label="console button collapsed"
aria-label="console button expanded"
className="expandCollapseButton"
role="button"
tabIndex={0}
@@ -340,9 +340,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
date
</span>
<span
aria-live="assertive"
className="message"
role="alert"
>
message
</span>

View File

@@ -109,7 +109,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
const q = params.toString();
const suffix = q !== "" ? `?${q}` : "";
const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`;
const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`;
return url.replace(/^http(s)?/, "ws$1");
};

View File

@@ -2,15 +2,12 @@
* Notebook container related stuff
*/
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { NotebookUtil } from "./NotebookUtil";
import { useNotebook } from "./useNotebook";
export class NotebookContainerClient {
@@ -45,7 +42,7 @@ export class NotebookContainerClient {
}, delayMs);
}
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
@@ -59,7 +56,7 @@ export class NotebookContainerClient {
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try {
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {
method: "GET",
headers: {
Authorization: authToken,
@@ -78,12 +75,6 @@ export class NotebookContainerClient {
freeKB: memoryUsageInfo.free,
};
}
} else if (NotebookUtil.isPhoenixEnabled()) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.ReConnect,
};
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(true);
}
return undefined;
} catch (error) {
@@ -93,13 +84,6 @@ export class NotebookContainerClient {
"Connection lost with Notebook server. Attempting to reconnect..."
);
}
if (NotebookUtil.isPhoenixEnabled()) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(true);
}
this.onConnectionLost();
return undefined;
}

View File

@@ -36,10 +36,7 @@ export class NotebookContentClient {
*
* @param parent parent folder
*/
public async createNewNotebookFile(
parent: NotebookContentItem,
isGithubTree?: boolean
): Promise<NotebookContentItem> {
public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise<NotebookContentItem> {
if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`);
}
@@ -102,6 +99,7 @@ export class NotebookContentClient {
if (!parent || parent.type !== NotebookContentItemType.Directory) {
throw new Error(`Parent must be a directory: ${parent}`);
}
const filepath = NotebookUtil.getFilePath(parent.path, name);
if (await this.checkIfFilepathExists(filepath)) {
throw new Error(`File already exists: ${filepath}`);

View File

@@ -212,7 +212,6 @@ export default class NotebookManager {
"Cancel",
() => reject(new Error("Commit dialog canceled")),
undefined,
undefined,
{
label: "Commit message",
autoAdjustHeight: true,

View File

@@ -3,7 +3,6 @@ import { AppState, selectors } from "@nteract/core";
import domtoimage from "dom-to-image";
import Html2Canvas from "html2canvas";
import path from "path";
import { userContext } from "../../UserContext";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as StringUtils from "../../Utils/StringUtils";
import { SnapshotFragment } from "./NotebookComponent/types";
@@ -329,16 +328,4 @@ export class NotebookUtil {
link.click();
document.body.removeChild(link);
}
public static getNotebookBtnTitle(fileName: string): string {
if (this.isPhoenixEnabled()) {
return `Download to ${fileName}`;
} else {
return `Download to my notebooks`;
}
}
public static isPhoenixEnabled(): boolean {
return userContext.features.notebooksTemporarilyDown === false && userContext.features.phoenix === true;
}
}

View File

@@ -2,12 +2,10 @@ import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType } 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 } from "../../Contracts/DataModels";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -16,7 +14,6 @@ import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import NotebookManager from "./NotebookManager";
import { NotebookUtil } from "./NotebookUtil";
interface NotebookState {
isNotebookEnabled: boolean;
@@ -31,10 +28,6 @@ interface NotebookState {
myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem;
connectionInfo: ContainerConnectionInfo;
notebookFolderName: string;
isAllocating: boolean;
isRefreshed: boolean;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -43,7 +36,6 @@ interface NotebookState {
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
setIsShellEnabled: (isShellEnabled: boolean) => void;
setNotebookBasePath: (notebookBasePath: string) => void;
setNotebookFolderName: (notebookFolderName: string) => void;
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
@@ -51,10 +43,6 @@ interface NotebookState {
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
setIsAllocating: (isAllocating: boolean) => void;
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo) => void;
setIsRefreshed: (isAllocating: boolean) => void;
}
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -77,12 +65,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined,
connectionInfo: {
status: ConnectionStatusType.Connect,
},
notebookFolderName: undefined,
isAllocating: false,
isRefreshed: false,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -93,7 +75,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
const { databaseAccount, authType } = userContext;
if (
@@ -187,10 +168,8 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = NotebookUtil.isPhoenixEnabled() === true ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName });
const myNotebooksContentRoot = {
name: get().notebookFolderName,
name: "My Notebooks",
path: get().notebookBasePath,
type: NotebookContentItemType.Directory,
};
@@ -206,7 +185,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
type: NotebookContentItemType.Directory,
}
: undefined;
set({
myNotebooksContentRoot,
galleryContentRoot,
@@ -268,15 +246,4 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
set({ gitHubNotebooksContentRoot });
}
},
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo): void => {
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: undefined,
authToken: undefined,
});
useNotebook.getState().setIsAllocating(false);
},
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
}));

View File

@@ -161,7 +161,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
true
).toLocaleLowerCase()}.`}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
@@ -210,7 +210,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
className="panelTextField"
aria-label="New database id"
autoFocus
tabIndex={0}
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ newDatabaseId: event.target.value })
@@ -237,7 +236,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
true
).toLocaleLowerCase()} within the database.`}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
)}
@@ -280,7 +279,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
@@ -363,7 +362,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
@@ -410,7 +409,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getPartitionKeyTooltipText()}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
@@ -468,7 +467,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
)}
@@ -498,7 +497,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
per partition key."
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
@@ -561,7 +560,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getAnalyticalStorageTooltipContent()}
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
@@ -638,7 +637,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge}
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>

View File

@@ -23,12 +23,10 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
export interface AddDatabasePaneProps {
explorer: Explorer;
buttonElement?: HTMLElement;
}
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
explorer: container,
buttonElement,
}: AddDatabasePaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
let throughput: number;
@@ -79,7 +77,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
dataExplorerArea: Constants.Areas.ContextualPane,
};
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
buttonElement.focus();
}, []);
const onSubmit = () => {

View File

@@ -198,7 +198,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<Stack className="panelGroupSpacing">
<TextField
aria-required="true"
required={true}
autoComplete="off"
styles={getTextFieldStyles()}
pattern="[^/?#\\]*[^/?# \\]"
@@ -286,7 +285,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
underlined
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
aria-required="true"
required={true}
ariaLabel="addCollection-tableId"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"

View File

@@ -5,7 +5,6 @@ import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
@@ -76,8 +75,6 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
selectedLocation.owner,
selectedLocation.repo
)} - ${selectedLocation.branch}`;
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
destination = useNotebook.getState().notebookFolderName;
}
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);

View File

@@ -12,7 +12,6 @@ import {
import React, { FormEvent, FunctionComponent } from "react";
import { IPinnedRepo } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { useNotebook } from "../../Notebook/useNotebook";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
interface Location {
@@ -47,10 +46,11 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
const getDropDownOptions = (): IDropdownOption[] => {
const options: IDropdownOption[] = [];
options.push({
key: "MyNotebooks-Item",
text: useNotebook.getState().notebookFolderName,
title: useNotebook.getState().notebookFolderName,
text: ResourceTreeAdapter.MyNotebooksTitle,
title: ResourceTreeAdapter.MyNotebooksTitle,
data: {
type: "MyNotebooks",
} as Location,

View File

@@ -108,8 +108,6 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
submitButtonText: "OK",
onSubmit,
};
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
return (
<RightPaneForm {...props}>
<div className="panelFormWrapper">
@@ -125,7 +123,6 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
onChange={(event, newInput?: string) => {
setInputCollectionName(newInput);
}}
ariaLabel={confirmContainer}
/>
</div>
{shouldRecordFeedback() && (
@@ -145,7 +142,6 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
onChange={(event, newInput?: string) => {
setDeleteCollectionFeedback(newInput);
}}
ariaLabel={reasonInfo}
/>
</div>
)}

View File

@@ -40,7 +40,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</span>
</Text>
<StyledTextFieldBase
ariaLabel="Confirm by typing the container id"
autoFocus={true}
id="confirmCollectionId"
onChange={[Function]}
@@ -54,7 +53,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
value=""
>
<TextFieldBase
ariaLabel="Confirm by typing the container id"
autoFocus={true}
deferredValidationTime={200}
id="confirmCollectionId"
@@ -348,7 +346,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<input
aria-invalid={false}
aria-label="Confirm by typing the container id"
autoFocus={true}
className="ms-TextField-field field-57"
id="confirmCollectionId"

View File

@@ -118,8 +118,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
const confirmDatabase = "Confirm by typing the database id";
const reasonInfo = "Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?";
return (
<RightPaneForm {...props}>
{!formError && <PanelInfoErrorComponent {...errorProps} />}
@@ -134,7 +133,6 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
onChange={(event, newInput?: string) => {
setDatabaseInput(newInput);
}}
ariaLabel={confirmDatabase}
/>
</div>
{isLastNonEmptyDatabase() && (
@@ -153,7 +151,6 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
onChange={(event, newInput?: string) => {
setDatabaseFeedbackInput(newInput);
}}
ariaLabel={reasonInfo}
/>
</div>
)}

View File

@@ -153,7 +153,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
selectedKey={paramKeyValue && paramKeyValue.key}
/>
))}
<Stack horizontal onClick={addNewParamAtLastIndex} tabIndex={0}>
<Stack horizontal onClick={addNewParamAtLastIndex}>
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
<Text className="addNewParamStyle">Add New Param</Text>
</Stack>

View File

@@ -59,36 +59,30 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
onChange={onParamKeyChange}
options={options}
styles={dropdownStyles}
tabIndex={0}
/>
<TextField
label={inputLabel && inputLabel}
id="confirmCollectionId"
autoFocus
value={paramValue}
onChange={onParamValueChange}
/>
{isAddRemoveVisible && (
<>
<div tabIndex={0}>
<Image
{...imageProps}
src={EntityCancelIcon}
alt="Delete param"
id="deleteparam"
role="button"
onClick={onDeleteParamKeyPress}
/>
</div>
<div tabIndex={0}>
<Image
{...imageProps}
src={AddPropertyIcon}
alt="Add param"
id="addparam"
role="button"
onClick={onAddNewParamKeyPress}
/>
</div>
<Image
{...imageProps}
src={EntityCancelIcon}
alt="Delete param"
id="deleteparam"
onClick={onDeleteParamKeyPress}
/>
<Image
{...imageProps}
src={AddPropertyIcon}
alt="Add param"
id="addparam"
onClick={onAddNewParamKeyPress}
/>
</>
)}
</Stack>

View File

@@ -23,7 +23,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],

View File

@@ -3,13 +3,13 @@
.panelFormWrapper {
display: flex;
flex-direction: column;
min-height: 100%;
height: 100%;
.panelMainContent {
flex-grow: 1;
padding: 0 34px;
margin: 20px 0;
overflow-x: hidden;
overflow: auto;
& > :not(.collapsibleSection) {
margin-bottom: @DefaultSpace;

View File

@@ -33,13 +33,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
{icon}
<span className="panelWarningErrorDetailsLinkContainer">
<Text
role="alert"
aria-live="assertive"
aria-label={message}
className="panelWarningErrorMessage"
variant="small"
>
<Text className="panelWarningErrorMessage" variant="small" aria-label="message">
{message}
{link && linkText && (
<Link target="_blank" href={link}>

View File

@@ -34,6 +34,6 @@ describe("Right Pane Form", () => {
it("should render error in header", () => {
render(<RightPaneForm {...props} formError="file already Exist" />);
expect(screen.getByLabelText("error")).toBeDefined();
expect(screen.getByRole("alert").innerHTML).toEqual("file already Exist");
expect(screen.getByLabelText("message").innerHTML).toEqual("file already Exist");
});
});

View File

@@ -195,6 +195,7 @@ export const SettingsPane: FunctionComponent = () => {
step={1}
className="textfontclr"
role="textbox"
tabIndex={0}
id="max-degree"
value={"" + maxDegreeOfParallelism}
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}

View File

@@ -123,6 +123,7 @@ exports[`Settings Pane should render Default properly 1`] = `
onValidate={[Function]}
role="textbox"
step={1}
tabIndex={0}
value="6"
/>
</div>

View File

@@ -13,7 +13,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],

View File

@@ -29,7 +29,6 @@ describe("Table query select Panel", () => {
it("Should checked availableCheckbox by default", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper.find("#availableCheckbox").first().props()).toEqual({
ariaPositionInSet: 0,
id: "availableCheckbox",
label: "Available Columns",
checked: true,

View File

@@ -128,9 +128,8 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
label="Available Columns"
checked={isAvailableColumnChecked}
onChange={availableColumnsCheckboxClick}
ariaPositionInSet={0}
/>
{columnOptions.map((column, index) => {
{columnOptions.map((column) => {
return (
<Checkbox
label={column.columnName}
@@ -138,7 +137,6 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
key={column.columnName}
checked={column.selected}
disabled={!column.editable}
ariaPositionInSet={index + 1}
/>
);
})}

View File

@@ -37,14 +37,12 @@ exports[`Table query select Panel should render Default properly 1`] = `
className="column-select-view"
>
<StyledCheckboxBase
ariaPositionInSet={0}
checked={true}
id="availableCheckbox"
label="Available Columns"
onChange={[Function]}
>
<CheckboxBase
ariaPositionInSet={0}
checked={true}
id="availableCheckbox"
label="Available Columns"
@@ -330,7 +328,6 @@ exports[`Table query select Panel should render Default properly 1`] = `
<input
aria-checked="true"
aria-label="Available Columns"
aria-posinset={0}
checked={true}
className="input-55"
data-ktp-execute-target={true}
@@ -649,7 +646,6 @@ exports[`Table query select Panel should render Default properly 1`] = `
</CheckboxBase>
</StyledCheckboxBase>
<StyledCheckboxBase
ariaPositionInSet={1}
checked={true}
disabled={false}
key=""
@@ -657,7 +653,6 @@ exports[`Table query select Panel should render Default properly 1`] = `
onChange={[Function]}
>
<CheckboxBase
ariaPositionInSet={1}
checked={true}
disabled={false}
label=""
@@ -944,7 +939,6 @@ exports[`Table query select Panel should render Default properly 1`] = `
aria-checked="true"
aria-disabled={false}
aria-label=""
aria-posinset={1}
checked={true}
className="input-55"
data-ktp-execute-target={true}

View File

@@ -327,17 +327,13 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
key=".0:$.1"
>
<Text
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
aria-live="assertive"
aria-label="message"
className="panelWarningErrorMessage"
role="alert"
variant="small"
>
<span
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
aria-live="assertive"
aria-label="message"
className="panelWarningErrorMessage css-56"
role="alert"
>
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
</span>
@@ -367,7 +363,6 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
</span>
</Text>
<StyledTextFieldBase
ariaLabel="Confirm by typing the database id"
autoFocus={true}
id="confirmDatabaseId"
onChange={[Function]}
@@ -380,7 +375,6 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
}
>
<TextFieldBase
ariaLabel="Confirm by typing the database id"
autoFocus={true}
deferredValidationTime={200}
id="confirmDatabaseId"
@@ -673,7 +667,6 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<input
aria-invalid={false}
aria-label="Confirm by typing the database id"
autoFocus={true}
className="ms-TextField-field field-60"
id="confirmDatabaseId"
@@ -714,7 +707,6 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
</span>
</Text>
<StyledTextFieldBase
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
id="deleteDatabaseFeedbackInput"
multiline={true}
onChange={[Function]}
@@ -728,7 +720,6 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
}
>
<TextFieldBase
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
deferredValidationTime={200}
id="deleteDatabaseFeedbackInput"
multiline={true}
@@ -1022,7 +1013,6 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<textarea
aria-invalid={false}
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
className="ms-TextField-field field-71"
id="deleteDatabaseFeedbackInput"
onBlur={[Function]}

View File

@@ -307,23 +307,16 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: AddDatabaseIcon,
title: "New " + getDatabaseName(),
description: undefined,
onClick: () => this.openAddDatabasePanel(),
onClick: () =>
useSidePanel
.getState()
.openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this.container} />),
});
}
return items;
}
private openAddDatabasePanel() {
const newDatabaseButton = document.activeElement as HTMLElement;
useSidePanel
.getState()
.openSidePanel(
"New " + getDatabaseName(),
<AddDatabasePanel explorer={this.container} buttonElement={newDatabaseButton} />
);
}
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
return {
iconSrc: NotebookIcon,

View File

@@ -1,4 +1,4 @@
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode, Text } from "@fluentui/react";
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode } from "@fluentui/react";
import React, { Fragment } from "react";
import SplitterLayout from "react-splitter-layout";
import "react-splitter-layout/lib/index.css";
@@ -120,13 +120,21 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
constructor(props: IQueryTabComponentProps) {
super(props);
const columns: IColumn[] = [
{
key: "column1",
name: "",
minWidth: 16,
maxWidth: 16,
data: String,
fieldName: "toolTip",
onRender: this.onRenderColumnItem,
},
{
key: "column2",
name: "METRIC",
minWidth: 200,
data: String,
fieldName: "metric",
onRender: this.onRenderColumnItem,
},
{
key: "column3",
@@ -198,12 +206,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
public onRenderColumnItem(item: IDocument): JSX.Element {
if (item.toolTip !== "") {
return (
<>
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
</>
);
return <InfoTooltip>{`${item.toolTip}`}</InfoTooltip>;
} else {
return undefined;
}
@@ -999,7 +1002,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
"data-order": 2,
"data-title": "Query Stats",
}}
style={{ height: "100%", overflowY: "scroll" }}
style={{ height: "100%" }}
>
{this.state.allResultsMetadata.length > 0 && !this.state.error && (
<div className="queryMetricsSummaryContainer">

View File

@@ -47,9 +47,7 @@
<img class="and-or-svg" src="/And-Or.svg" alt="Group selected clauses" />
</button>
</th>
<th class="clause-table-cell header-background">
<!-- Grouping indicator -->
</th>
<th class="clause-table-cell header-background"><!-- Grouping indicator --></th>
<th class="clause-table-cell header-background and-or-header">
<span data-bind="text: andLabel"></span>
</th>
@@ -138,7 +136,7 @@
class="select-options-link"
data-bind="click: selectQueryOptions, event: { keydown: onselectQueryOptionsKeyDown }"
tabindex="0"
role="button"
role="link"
>
<span>Choose Columns... </span>
</a>

View File

@@ -1,5 +1,5 @@
import ko from "knockout";
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
import errorIcon from "../../../images/close-black.svg";
import { useObservable } from "../../hooks/useObservable";
@@ -32,13 +32,7 @@ export const Tabs = (): JSX.Element => {
function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
const [hovering, setHovering] = useState(false);
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
useEffect(() => {
if (active && focusTab.current) {
focusTab.current.focus();
}
});
return (
<li
onMouseOver={() => setHovering(true)}
@@ -52,7 +46,6 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
aria-controls={tab.tabId}
tabIndex={0}
role="tab"
ref={focusTab}
>
<span className="tabNavContentContainer">
<a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}>

View File

@@ -291,7 +291,7 @@ export default class UserDefinedFunctionTabContent extends Component<
language={"javascript"}
content={udfBody}
isReadOnly={false}
ariaLabel={"User defined function body"}
ariaLabel={"Graph JSON"}
onContentChanged={this.handleUdfBodyChange}
/>
</div>

View File

@@ -11,7 +11,7 @@ import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants";
import { Areas, Notebook } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
@@ -128,20 +128,17 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
notebooksTree.children.push(buildGalleryNotebooksTree());
}
if (
myNotebooksContentRoot &&
((NotebookUtil.isPhoenixEnabled() &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected) ||
userContext.features.phoenix === false)
) {
if (myNotebooksContentRoot) {
notebooksTree.children.push(buildMyNotebooksTree());
}
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
// collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(buildGitHubNotebooksTree(true));
notebooksTree.children.push(buildGitHubNotebooksTree());
}
}
return notebooksTree;
};
@@ -167,11 +164,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
myNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (
hasOpened &&
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === false
) {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
@@ -185,23 +178,20 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
return myNotebooksTree;
};
const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => {
const buildGitHubNotebooksTree = (): TreeNode => {
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (
hasOpened &&
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === false
) {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
},
true
);
const manageGitContextMenu: TreeNodeMenuItem[] = [
gitHubNotebooksTree.contextMenu = [
{
label: "Manage GitHub settings",
onClick: () =>
@@ -226,7 +216,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
},
},
];
gitHubNotebooksTree.contextMenu = manageGitContextMenu;
gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true;

View File

@@ -130,8 +130,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
path: "Gallery",
type: NotebookContentItemType.File,
};
this.myNotebooksContentRoot = {
name: useNotebook.getState().notebookFolderName,
name: ResourceTreeAdapter.MyNotebooksTitle,
path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory,
};
@@ -145,11 +146,16 @@ export class ResourceTreeAdapter implements ReactAdapter {
})
);
}
this.gitHubNotebooksContentRoot = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: ResourceTreeAdapter.PseudoDirPath,
type: NotebookContentItemType.Directory,
};
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
this.gitHubNotebooksContentRoot = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: ResourceTreeAdapter.PseudoDirPath,
type: NotebookContentItemType.Directory,
};
} else {
this.gitHubNotebooksContentRoot = undefined;
}
return Promise.all(refreshTasks);
}

View File

@@ -1,57 +0,0 @@
{
"GraphAPIDescription": "Provision a Graph API Compute for your Azure Cosmos DB account.",
"GraphAPICompute": "Graph GraphAPI Compute",
"Provisioned": "Provisioned",
"Deprovisioned": "Deprovisioned",
"Compute": "Compute",
"GremlinV2": "GremlinV2",
"LearnAboutCompute": "Learn more about GraphAPI Compute.",
"DeprovisioningDetailsText": "Learn more about deprovisioning the GraphAPI Compute.",
"ComputePricing": "Learn more about GraphAPI Compute pricing.",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"NumberOfInstances": "Number of instances",
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
"CreateMessage": "Graph GraphAPI Compute resource is being created.",
"CreateInitializeTitle": "Provisioning resource",
"CreateInitializeMessage": "GraphAPI Compute resource will be provisioned.",
"CreateSuccessTitle": "Resource provisioned",
"CreateSuccesseMessage": "GraphAPI Compute resource provisioned.",
"CreateFailureTitle": "Failed to provision resource",
"CreateFailureMessage": "GraphAPI Compute resource provisioning failed.",
"UpdateMessage": "GraphAPI Compute resource is being updated.",
"UpdateInitializeTitle": "Updating resource",
"UpdateInitializeMessage": "GraphAPI Compute resource will be updated.",
"UpdateSuccessTitle": "Resource updated",
"UpdateSuccesseMessage": "GraphAPI Compute resource updated.",
"UpdateFailureTitle": "Failed to update resource",
"UpdateFailureMessage": "GraphAPI Compute resource updation failed.",
"DeleteMessage": "GraphAPI Compute resource is being deleted.",
"DeleteInitializeTitle": "Deleting resource",
"DeleteInitializeMessage": "GraphAPI Compute resource will be deleted.",
"DeleteSuccessTitle": "Resource deleted",
"DeleteSuccesseMessage": "GraphAPI Compute resource deleted.",
"DeleteFailureTitle": "Failed to delete resource",
"DeleteFailureMessage": "GraphAPI Compute resource deletion failed.",
"CannotSave": "Cannot save the changes to the GraphAPI Compute resource at the moment.",
"GraphAccountEndpoint": "Graph Account Endpoint",
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"ApproximateCost": "Approximate Cost Per Hour",
"CostText": "Hourly cost of the GraphAPI Compute resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String",
"ConnectionStringText": "To use the GraphAPI Compute, use the connection string shown in ",
"KeysBlade": "the keys blade.",
"MetricsString": "Metrics",
"MetricsText": "Monitor the CPU and memory usage for the GraphAPI Compute instances in ",
"MetricsBlade": "the metrics blade.",
"MonitorUsage": "Monitor Usage",
"ResizingDecisionText": "Number of instances has to be 1 during provisioning. Instances can only be incremented by 1 at once. ",
"ResizingDecisionLink": "Learn more about GraphAPI Compute sizing.",
"WarningBannerOnUpdate": "Adding or modifying GraphAPI Compute instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the GraphAPI Compute, you will not be able to connect to the Graph API account."
}

View File

@@ -37,7 +37,6 @@ import "./Explorer/Controls/TreeComponent/treeComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";

View File

@@ -1,67 +0,0 @@
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext";
import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
export interface IPhoenixResponse<T> {
status: number;
data: T;
}
export interface IPhoenixConnectionInfoResult {
readonly notebookAuthToken?: string;
readonly notebookServerUrl?: string;
}
export interface IProvosionData {
cosmosEndpoint: string;
dbAccountName: string;
aadToken: string;
resourceGroup: string;
subscriptionId: string;
}
export class PhoenixClient {
public async containerConnectionInfo(
provisionData: IProvosionData
): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> {
try {
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/allocate`, {
method: "POST",
headers: PhoenixClient.getHeaders(),
body: JSON.stringify(provisionData),
});
let data: IPhoenixConnectionInfoResult;
if (response.status === HttpStatusCodes.OK) {
data = await response.json();
}
return {
status: response.status,
data,
};
} catch (error) {
console.error(error);
throw error;
}
}
public static getPhoenixEndpoint(): string {
const phoenixEndpoint =
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
console.error(error);
throw new Error(error);
}
return phoenixEndpoint;
}
public getPhoenixContainerPoolingEndPoint(): string {
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer`;
}
private static getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader();
return {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
};
}
}

View File

@@ -1,4 +1,4 @@
import { extractFeatures, hasFlag } from "./extractFeatures";
import { extractFeatures } from "./extractFeatures";
describe("extractFeatures", () => {
it("correctly detects feature flags in a case insensitive manner", () => {
@@ -14,25 +14,9 @@ describe("extractFeatures", () => {
});
const features = extractFeatures(params);
expect(features.notebookServerUrl).toBe(url);
expect(features.notebookServerToken).toBe(token);
expect(features.enableNotebooks).toBe(notebooksEnabled);
});
});
describe("hasFlag", () => {
it("correctly determines if value has flag", () => {
const desiredFlag = "readDocument";
const singleFlagValue = "readDocument";
const multipleFlagValues = "readDocument|createDocument";
const differentFlagValue = "createDocument";
expect(hasFlag(singleFlagValue, desiredFlag)).toBe(true);
expect(hasFlag(multipleFlagValues, desiredFlag)).toBe(true);
expect(hasFlag(differentFlagValue, desiredFlag)).toBe(false);
expect(hasFlag(multipleFlagValues, (undefined as unknown) as string)).toBe(false);
expect(hasFlag((undefined as unknown) as string, desiredFlag)).toBe(false);
expect(hasFlag((undefined as unknown) as string, (undefined as unknown) as string)).toBe(false);
});
});

View File

@@ -11,7 +11,6 @@ export type Features = {
autoscaleDefault: boolean;
partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
phoenix: boolean;
readonly enableSDKoperations: boolean;
readonly enableSpark: boolean;
readonly enableTtl: boolean;
@@ -20,7 +19,6 @@ export type Features = {
readonly enableKoResourceTree: boolean;
readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string;
readonly phoenixEndpoint?: string;
readonly livyEndpoint?: string;
readonly notebookBasePath?: string;
readonly notebookServerToken?: string;
@@ -30,8 +28,6 @@ export type Features = {
readonly pr?: string;
readonly showMinRUSurvey: boolean;
readonly ttl90Days: boolean;
readonly mongoProxyEndpoint?: string;
readonly mongoProxyAPIs?: string;
readonly notebooksTemporarilyDown: boolean;
};
@@ -66,10 +62,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableKoResourceTree: "true" === get("enablekoresourcetree"),
executeSproc: "true" === get("dataexplorerexecutesproc"),
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
mongoProxyEndpoint: get("mongoproxyendpoint"),
mongoProxyAPIs: get("mongoproxyapis"),
junoEndpoint: get("junoendpoint"),
phoenixEndpoint: get("phoenixendpoint"),
livyEndpoint: get("livyendpoint"),
notebookBasePath: get("notebookbasepath"),
notebookServerToken: get("notebookservertoken"),
@@ -83,15 +76,5 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
partitionKeyDefault: "true" === get("partitionkeytest"),
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"),
phoenix: "true" === get("phoenix"),
};
}
export function hasFlag(flags: string | undefined, desiredFlag: string | undefined): boolean {
if (!flags || !desiredFlag) {
return false;
}
const features = flags.split("|");
return features.find((feature) => feature === desiredFlag) ? true : false;
}

View File

@@ -1,200 +0,0 @@
import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext";
import { armRequestWithoutPolling } from "../../Utils/arm/request";
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
import { RefreshResult } from "../SelfServeTypes";
import GraphAPICompute from "./GraphAPICompute";
import {
FetchPricesResponse,
RegionsResponse,
GraphAPIComputeServiceResource,
UpdateComputeRequestParameters,
} from "./GraphAPICompute.types";
const apiVersion = "2021-04-01-preview";
const gremlinV2 = "GremlinV2";
export enum ResourceStatus {
Running = "Running",
Creating = "Creating",
Updating = "Updating",
Deleting = "Deleting",
}
export interface ComputeResponse {
sku: string;
instances: number;
status: string;
endpoint: string;
}
export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/${gremlinV2}`;
};
export const updateComputeResource = async (sku: string, instances: number): Promise<string> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
const body: UpdateComputeRequestParameters = {
properties: {
instanceSize: sku,
instanceCount: instances,
serviceType: gremlinV2,
},
};
const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: GraphAPICompute.name };
const updateTimeStamp = selfServeTraceStart(telemetryData);
let armRequestResult;
try {
armRequestResult = await armRequestWithoutPolling({
host: configContext.ARM_ENDPOINT,
path,
method: "PUT",
apiVersion,
body,
});
selfServeTraceSuccess(telemetryData, updateTimeStamp);
} catch (e) {
const failureTelemetry = { ...body, e, selfServeClassName: GraphAPICompute.name };
selfServeTraceFailure(failureTelemetry, updateTimeStamp);
throw e;
}
return armRequestResult?.operationStatusUrl;
};
export const deleteComputeResource = async (): Promise<string> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
const telemetryData = { httpMethod: "DELETE", selfServeClassName: GraphAPICompute.name };
const deleteTimeStamp = selfServeTraceStart(telemetryData);
let armRequestResult;
try {
armRequestResult = await armRequestWithoutPolling({
host: configContext.ARM_ENDPOINT,
path,
method: "DELETE",
apiVersion,
});
selfServeTraceSuccess(telemetryData, deleteTimeStamp);
} catch (e) {
const failureTelemetry = { e, selfServeClassName: GraphAPICompute.name };
selfServeTraceFailure(failureTelemetry, deleteTimeStamp);
throw e;
}
return armRequestResult?.operationStatusUrl;
};
export const getComputeResource = async (): Promise<GraphAPIComputeServiceResource> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
const telemetryData = { httpMethod: "GET", selfServeClassName: GraphAPICompute.name };
const getResourceTimeStamp = selfServeTraceStart(telemetryData);
let armRequestResult;
try {
armRequestResult = await armRequestWithoutPolling<GraphAPIComputeServiceResource>({
host: configContext.ARM_ENDPOINT,
path,
method: "GET",
apiVersion,
});
selfServeTraceSuccess(telemetryData, getResourceTimeStamp);
} catch (e) {
const failureTelemetry = { e, selfServeClassName: GraphAPICompute.name };
selfServeTraceFailure(failureTelemetry, getResourceTimeStamp);
throw e;
}
return armRequestResult?.result;
};
export const getCurrentProvisioningState = async (): Promise<ComputeResponse> => {
try {
const response = await getComputeResource();
return {
sku: response.properties.instanceSize,
instances: response.properties.instanceCount,
status: response.properties.status,
endpoint: response.properties.GraphAPIComputeEndPoint,
};
} catch (e) {
return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined };
}
};
export const refreshComputeProvisioning = async (): Promise<RefreshResult> => {
try {
const response = await getComputeResource();
if (response.properties.status === ResourceStatus.Running.toString()) {
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
} else if (response.properties.status === ResourceStatus.Creating.toString()) {
return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" };
} else if (response.properties.status === ResourceStatus.Deleting.toString()) {
return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" };
} else {
return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" };
}
} catch {
//TODO differentiate between different failures
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
}
};
const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
};
export const getReadRegions = async (): Promise<Array<string>> => {
try {
const readRegions = new Array<string>();
const response = await armRequestWithoutPolling<RegionsResponse>({
host: configContext.ARM_ENDPOINT,
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
method: "GET",
apiVersion: "2021-04-01-preview",
});
if (response.result.location !== undefined) {
readRegions.push(response.result.location.split(" ").join("").toLowerCase());
} else {
for (const location of response.result.locations) {
readRegions.push(location.locationName.split(" ").join("").toLowerCase());
}
}
return readRegions;
} catch (err) {
return new Array<string>();
}
};
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
};
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
try {
const priceMap = new Map<string, Map<string, number>>();
for (const region of regions) {
const regionPriceMap = new Map<string, number>();
const response = await armRequestWithoutPolling<FetchPricesResponse>({
host: configContext.ARM_ENDPOINT,
path: getFetchPricesPathForRegion(userContext.subscriptionId),
method: "POST",
apiVersion: "2020-01-01-preview",
queryParams: {
filter:
"armRegionNameeq '" +
region +
"'andserviceFamilyeq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
},
});
for (const item of response.result.Items) {
regionPriceMap.set(item.skuName, item.retailPrice);
}
priceMap.set(region, regionPriceMap);
}
return priceMap;
} catch (err) {
return undefined;
}
};

View File

@@ -1,423 +0,0 @@
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
import { selfServeTrace } from "../SelfServeTelemetryProcessor";
import {
ChoiceItem,
Description,
DescriptionType,
Info,
InputType,
NumberUiType,
OnSaveResult,
RefreshResult,
SelfServeBaseClass,
SmartUiInput,
} from "../SelfServeTypes";
import { BladeType, generateBladeLink } from "../SelfServeUtils";
import {
deleteComputeResource,
getCurrentProvisioningState,
getPriceMap,
getReadRegions,
refreshComputeProvisioning,
updateComputeResource,
} from "./GraphAPICompute.rp";
const costPerHourDefaultValue: Description = {
textTKey: "CostText",
type: DescriptionType.Text,
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing",
textTKey: "ComputePricing",
},
};
const connectionStringValue: Description = {
textTKey: "ConnectionStringText",
type: DescriptionType.Text,
link: {
href: generateBladeLink(BladeType.SqlKeys),
textTKey: "KeysBlade",
},
};
const metricsStringValue: Description = {
textTKey: "MetricsText",
type: DescriptionType.Text,
link: {
href: generateBladeLink(BladeType.Metrics),
textTKey: "MetricsBlade",
},
};
const CosmosD4s = "Cosmos.D4s";
const CosmosD8s = "Cosmos.D8s";
const CosmosD16s = "Cosmos.D16s";
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
currentValues.set("sku", { value: newValue });
currentValues.set("costPerHour", {
value: calculateCost(newValue as string, currentValues.get("instances").value as number),
});
return currentValues;
};
const onNumberOfInstancesChange = (
newValue: InputType,
currentValues: Map<string, SmartUiInput>,
baselineValues: Map<string, SmartUiInput>
): Map<string, SmartUiInput> => {
currentValues.set("instances", { value: newValue });
const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean;
const baselineInstances = baselineValues.get("instances")?.value as number;
if (!ComputeOriginallyEnabled || baselineInstances !== newValue) {
currentValues.set("warningBanner", {
value: {
textTKey: "WarningBannerOnUpdate",
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-overview",
textTKey: "ComputePricing",
},
} as Description,
hidden: false,
});
} else {
currentValues.set("warningBanner", undefined);
}
currentValues.set("costPerHour", {
value: calculateCost(currentValues.get("sku").value as string, newValue as number),
});
return currentValues;
};
const onEnableComputeChange = (
newValue: InputType,
currentValues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
): Map<string, SmartUiInput> => {
currentValues.set("enableCompute", { value: newValue });
const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean;
if (ComputeOriginallyEnabled === newValue) {
currentValues.set("sku", baselineValues.get("sku"));
currentValues.set("instances", baselineValues.get("instances"));
currentValues.set("costPerHour", baselineValues.get("costPerHour"));
currentValues.set("warningBanner", baselineValues.get("warningBanner"));
currentValues.set("connectionString", baselineValues.get("connectionString"));
currentValues.set("metricsString", baselineValues.get("metricsString"));
return currentValues;
}
currentValues.set("warningBanner", undefined);
if (newValue === true) {
currentValues.set("warningBanner", {
value: {
textTKey: "WarningBannerOnUpdate",
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //needs updating
textTKey: "ComputePricing",
},
} as Description,
hidden: false,
});
currentValues.set("costPerHour", {
value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number),
hidden: false,
});
} else {
currentValues.set("warningBanner", {
value: {
textTKey: "WarningBannerOnDelete",
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", // needs updating
textTKey: "DeprovisioningDetailsText",
},
} as Description,
hidden: false,
});
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
}
const sku = currentValues.get("sku");
const hideAttributes = newValue === undefined || !(newValue as boolean);
currentValues.set("sku", {
value: sku.value,
hidden: hideAttributes,
disabled: ComputeOriginallyEnabled,
});
currentValues.set("instances", {
value: 1,
hidden: hideAttributes,
disabled: true,
});
currentValues.set("connectionString", {
value: connectionStringValue,
hidden: !newValue || !ComputeOriginallyEnabled,
});
currentValues.set("metricsString", {
value: metricsStringValue,
hidden: !newValue || !ComputeOriginallyEnabled,
});
return currentValues;
};
const skuDropDownItems: ChoiceItem[] = [
{ labelTKey: "CosmosD4s", key: CosmosD4s },
{ labelTKey: "CosmosD8s", key: CosmosD8s },
{ labelTKey: "CosmosD16s", key: CosmosD16s },
];
const getSkus = async (): Promise<ChoiceItem[]> => {
return skuDropDownItems;
};
const NumberOfInstancesDropdownInfo: Info = {
messageTKey: "ResizingDecisionText",
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-size", // todo
textTKey: "ResizingDecisionLink",
},
};
const getInstancesMin = async (): Promise<number> => {
return 1;
};
const getInstancesMax = async (): Promise<number> => {
return 5;
};
const ApproximateCostDropDownInfo: Info = {
messageTKey: "CostText",
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //todo
textTKey: "ComputePricing",
},
};
let priceMap: Map<string, Map<string, number>>;
let regions: Array<string>;
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.", ""));
if (incrementalCost === undefined) {
throw new Error("Value not found in map.");
}
costPerHour += incrementalCost;
}
costPerHour *= instanceCount;
costPerHour = Math.round(costPerHour * 100) / 100;
return {
textTKey: `${costPerHour} USD`,
type: DescriptionType.Text,
};
} catch (err) {
return costPerHourDefaultValue;
}
};
@IsDisplayable()
@RefreshOptions({ retryIntervalInMs: 20000 })
export default class GraphAPICompute extends SelfServeBaseClass {
public onRefresh = async (): Promise<RefreshResult> => {
return await refreshComputeProvisioning();
};
public onSave = async (
currentValues: Map<string, SmartUiInput>,
baselineValues: Map<string, SmartUiInput>
): Promise<OnSaveResult> => {
selfServeTrace({ selfServeClassName: GraphAPICompute.name });
const ComputeCurrentlyEnabled = currentValues.get("enableCompute")?.value as boolean;
const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean;
currentValues.set("warningBanner", undefined);
if (ComputeOriginallyEnabled) {
if (!ComputeCurrentlyEnabled) {
const operationStatusUrl = await deleteComputeResource();
return {
operationStatusUrl: operationStatusUrl,
portalNotification: {
initialize: {
titleTKey: "DeleteInitializeTitle",
messageTKey: "DeleteInitializeMessage",
},
success: {
titleTKey: "DeleteSuccessTitle",
messageTKey: "DeleteSuccesseMessage",
},
failure: {
titleTKey: "DeleteFailureTitle",
messageTKey: "DeleteFailureMessage",
},
},
};
} else {
const sku = currentValues.get("sku")?.value as string;
const instances = currentValues.get("instances").value as number;
const operationStatusUrl = await updateComputeResource(sku, instances);
return {
operationStatusUrl: operationStatusUrl,
portalNotification: {
initialize: {
titleTKey: "UpdateInitializeTitle",
messageTKey: "UpdateInitializeMessage",
},
success: {
titleTKey: "UpdateSuccessTitle",
messageTKey: "UpdateSuccesseMessage",
},
failure: {
titleTKey: "UpdateFailureTitle",
messageTKey: "UpdateFailureMessage",
},
},
};
}
} else {
const sku = currentValues.get("sku")?.value as string;
const instances = currentValues.get("instances").value as number;
const operationStatusUrl = await updateComputeResource(sku, instances);
return {
operationStatusUrl: operationStatusUrl,
portalNotification: {
initialize: {
titleTKey: "CreateInitializeTitle",
messageTKey: "CreateInitializeMessage",
},
success: {
titleTKey: "CreateSuccessTitle",
messageTKey: "CreateSuccesseMessage",
},
failure: {
titleTKey: "CreateFailureTitle",
messageTKey: "CreateFailureMessage",
},
},
};
}
};
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
// Based on the RP call enableCompute will be true if it has not yet been enabled and false if it has.
const defaults = new Map<string, SmartUiInput>();
defaults.set("enableCompute", { value: false });
defaults.set("sku", { value: CosmosD4s, hidden: true });
defaults.set("instances", { value: 1, hidden: true });
defaults.set("costPerHour", undefined);
defaults.set("connectionString", undefined);
defaults.set("metricsString", {
value: undefined,
hidden: true,
});
regions = await getReadRegions();
priceMap = await getPriceMap(regions);
const response = await getCurrentProvisioningState();
if (response.status && response.status === "Creating") {
defaults.set("enableCompute", { value: true });
defaults.set("sku", { value: response.sku, disabled: true });
defaults.set("instances", { value: response.instances, disabled: true });
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
defaults.set("connectionString", {
value: connectionStringValue,
hidden: true,
});
defaults.set("metricsString", {
value: metricsStringValue,
hidden: true,
});
} else if (response.status && response.status !== "Deleting") {
defaults.set("enableCompute", { value: true });
defaults.set("sku", { value: response.sku, disabled: true });
defaults.set("instances", { value: response.instances });
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
defaults.set("connectionString", {
value: connectionStringValue,
hidden: false,
});
defaults.set("metricsString", {
value: metricsStringValue,
hidden: false,
});
}
defaults.set("warningBanner", undefined);
return defaults;
};
@Values({
isDynamicDescription: true,
})
warningBanner: string;
@Values({
description: {
textTKey: "GraphAPIDescription",
type: DescriptionType.Text,
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", //todo
textTKey: "LearnAboutCompute",
},
},
})
description: string;
@OnChange(onEnableComputeChange)
@Values({
labelTKey: "Compute",
trueLabelTKey: "Provisioned",
falseLabelTKey: "Deprovisioned",
})
enableCompute: boolean;
@OnChange(onSKUChange)
@Values({
labelTKey: "SKUs",
choices: getSkus,
placeholderTKey: "SKUsPlaceHolder",
})
sku: ChoiceItem;
@OnChange(onNumberOfInstancesChange)
@PropertyInfo(NumberOfInstancesDropdownInfo)
@Values({
labelTKey: "NumberOfInstances",
min: getInstancesMin,
max: getInstancesMax,
step: 1,
uiType: NumberUiType.Spinner,
})
instances: number;
@PropertyInfo(ApproximateCostDropDownInfo)
@Values({
labelTKey: "ApproximateCost",
isDynamicDescription: true,
})
costPerHour: string;
@Values({
labelTKey: "ConnectionString",
isDynamicDescription: true,
})
connectionString: string;
@Values({
labelTKey: "MonitorUsage",
description: metricsStringValue,
})
metricsString: string;
}

View File

@@ -1,65 +0,0 @@
export enum Regions {
NorthCentralUS = "NorthCentralUS",
WestUS = "WestUS",
EastUS2 = "EastUS2",
}
export interface AccountProps {
regions: Regions;
enableLogging: boolean;
accountName: string;
collectionThroughput: number;
dbThroughput: number;
}
export type GraphAPIComputeServiceResource = {
id: string;
name: string;
type: string;
properties: GraphAPIComputeServiceProps;
locations: GraphAPIComputeServiceLocations;
};
export type GraphAPIComputeServiceProps = {
serviceType: string;
creationTime: string;
status: string;
instanceSize: string;
instanceCount: number;
GraphAPIComputeEndPoint: string;
};
export type GraphAPIComputeServiceLocations = {
location: string;
status: string;
GraphAPIComputeEndpoint: string;
};
export type UpdateComputeRequestParameters = {
properties: UpdateComputeRequestProperties;
};
export type UpdateComputeRequestProperties = {
instanceSize: string;
instanceCount: number;
serviceType: string;
};
export type FetchPricesResponse = {
Items: Array<PriceItem>;
NextPageLink: string | undefined;
Count: number;
};
export type PriceItem = {
retailPrice: number;
skuName: string;
};
export type RegionsResponse = {
locations: Array<RegionItem>;
location: string;
};
export type RegionItem = {
locationName: string;
};

View File

@@ -50,14 +50,6 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
await loadTranslations(sqlX.constructor.name);
return sqlX.toSelfServeDescriptor();
}
case SelfServeType.graphapicompute: {
const GraphAPICompute = await import(
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
);
const graphAPICompute = new GraphAPICompute.default();
await loadTranslations(graphAPICompute.constructor.name);
return graphAPICompute.toSelfServeDescriptor();
}
default:
return undefined;
}

View File

@@ -31,7 +31,6 @@ export enum SelfServeType {
// Add your self serve types here
example = "example",
sqlx = "sqlx",
graphapicompute = "graphapicompute",
}
/**

View File

@@ -138,17 +138,9 @@ 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>> => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "getRegions",
description: "",
selfServeClassName: SqlX.name,
};
const getRegionsTimestamp = selfServeTraceStart(telemetryData);
export const getReadRegions = async (): Promise<Array<string>> => {
try {
const regions = new Array<string>();
const readRegions = new Array<string>();
const response = await armRequestWithoutPolling<RegionsResponse>({
host: configContext.ARM_ENDPOINT,
@@ -158,18 +150,14 @@ export const getRegions = async (): Promise<Array<string>> => {
});
if (response.result.location !== undefined) {
regions.push(response.result.location.split(" ").join("").toLowerCase());
readRegions.push(response.result.location.replace(" ", "").toLowerCase());
} else {
for (const location of response.result.locations) {
regions.push(location.locationName.split(" ").join("").toLowerCase());
readRegions.push(location.locationName.replace(" ", "").toLowerCase());
}
}
selfServeTraceSuccess(telemetryData, getRegionsTimestamp);
return regions;
return readRegions;
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
return new Array<string>();
}
};
@@ -179,14 +167,6 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => {
};
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "getPriceMap",
description: "fetch prices API call",
selfServeClassName: SqlX.name,
};
const getPriceMapTimestamp = selfServeTraceStart(telemetryData);
try {
const priceMap = new Map<string, Map<string, number>>();
@@ -212,12 +192,8 @@ export const getPriceMap = async (regions: Array<string>): Promise<Map<string, M
priceMap.set(region, regionPriceMap);
}
selfServeTraceSuccess(telemetryData, getPriceMapTimestamp);
return priceMap;
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getPriceMapTimestamp);
return undefined;
}
};

View File

@@ -1,10 +1,5 @@
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
import {
selfServeTrace,
selfServeTraceFailure,
selfServeTraceStart,
selfServeTraceSuccess,
} from "../SelfServeTelemetryProcessor";
import { selfServeTrace } from "../SelfServeTelemetryProcessor";
import {
ChoiceItem,
Description,
@@ -22,7 +17,7 @@ import {
deleteDedicatedGatewayResource,
getCurrentProvisioningState,
getPriceMap,
getRegions,
getReadRegions,
refreshDedicatedGatewayProvisioning,
updateDedicatedGatewayResource,
} from "./SqlX.rp";
@@ -210,14 +205,6 @@ let priceMap: Map<string, Map<string, number>>;
let regions: Array<string>;
const calculateCost = (skuName: string, instanceCount: number): Description => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "calculateCost",
description: "performs final calculation",
selfServeClassName: SqlX.name,
};
const calculateCostTimestamp = selfServeTraceStart(telemetryData);
try {
let costPerHour = 0;
for (const region of regions) {
@@ -228,22 +215,14 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
costPerHour += incrementalCost;
}
if (costPerHour === 0) {
throw new Error("Cost per hour = 0");
}
costPerHour *= instanceCount;
costPerHour = Math.round(costPerHour * 100) / 100;
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
return {
textTKey: `${costPerHour} USD`,
type: DescriptionType.Text,
};
} catch (err) {
const failureTelemetry = { err, regions, priceMap, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, calculateCostTimestamp);
return costPerHourDefaultValue;
}
};
@@ -345,7 +324,7 @@ export default class SqlX extends SelfServeBaseClass {
hidden: true,
});
regions = await getRegions();
regions = await getReadRegions();
priceMap = await getPriceMap(regions);
const response = await getCurrentProvisioningState();

View File

@@ -125,8 +125,7 @@ export class OfferPricing {
S3Price: 0.1344,
Standard: {
StartingPrice: 24 / hoursInAMonth, // per hour
SingleMasterPricePerRU: 0.00008,
MultiMasterPricePerRU: 0.00016,
PricePerRU: 0.00008,
PricePerGB: 0.25 / hoursInAMonth,
},
},
@@ -138,8 +137,7 @@ export class OfferPricing {
S3Price: 0.6,
Standard: {
StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour
SingleMasterPricePerRU: 0.00051,
MultiMasterPricePerRU: 0.00102,
PricePerRU: 0.00051,
PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth,
},
},

View File

@@ -2,7 +2,9 @@ import * as DataModels from "../Contracts/DataModels";
import { userContext } from "../UserContext";
export class DefaultExperienceUtility {
public static getApiKindFromDefaultExperience(defaultExperience: typeof userContext.apiType): DataModels.ApiKind {
public static getApiKindFromDefaultExperience(
defaultExperience: typeof userContext.apiType | null
): DataModels.ApiKind {
if (!defaultExperience) {
return DataModels.ApiKind.SQL;
}

View File

@@ -2,11 +2,11 @@ import * as Constants from "./Constants";
export function computeRUUsagePrice(serverId: string, requestUnits: number): string {
if (serverId === "mooncake") {
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.SingleMasterPricePerRU;
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU;
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency;
}
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.SingleMasterPricePerRU;
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU;
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
}

View File

@@ -1,3 +1,7 @@
import * as Constants from "../Common/Constants";
export const manualToAutoscaleDisclaimer = `The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s. <a href="${Constants.Urls.autoscaleMigration}">Learn more</a>.`;
export const minAutoPilotThroughput = 4000;
export const autoPilotIncrementStep = 1000;

View File

@@ -2,7 +2,6 @@ import { HttpStatusCodes } from "../Common/Constants";
import { useDialog } from "../Explorer/Controls/Dialog";
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import * as GalleryUtils from "./GalleryUtils";
@@ -35,7 +34,7 @@ describe("GalleryUtils", () => {
expect(useDialog.getState().visible).toBe(true);
expect(useDialog.getState().dialogProps).toBeDefined();
expect(useDialog.getState().dialogProps.title).toBe(`Download to ${useNotebook.getState().notebookFolderName}`);
expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks");
});
it("favoriteItem favorites item", async () => {

View File

@@ -10,8 +10,6 @@ import {
SortBy,
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor";
@@ -225,90 +223,66 @@ export function downloadItem(
const name = data.name;
useDialog.getState().showOkCancelModalDialog(
`Download to ${useNotebook.getState().notebookFolderName}`,
undefined,
"Download to My Notebooks",
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
"Download",
async () => {
if (NotebookUtil.isPhoenixEnabled()) {
await container.allocateContainer();
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
downloadNotebookItem(name, data, junoClient, container, onComplete);
} else {
useDialog
.getState()
.showOkModalDialog(
"Failed to Connect",
"Failed to connect to temporary workspace. Please refresh the page and try again."
);
}
},
"Cancel",
undefined,
container.getDownloadModalConent(name)
);
}
export async function downloadNotebookItem(
fileName: string,
data: IGalleryItem,
junoClient: JunoClient,
container: Explorer,
onComplete: (item: IGalleryItem) => void
) {
const clearInProgressMessage = logConsoleProgress(
`Downloading ${fileName} to ${useNotebook.getState().notebookFolderName}`
);
const startKey = traceStart(Action.NotebooksGalleryDownload, {
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
});
try {
const response = await junoClient.getNotebookContent(data.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
}
const notebook = JSON.parse(response.data) as Notebook;
removeNotebookViewerLink(notebook, data.newCellId);
if (!data.isSample) {
const metadata = notebook.metadata as { [name: string]: unknown };
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`);
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
if (increaseDownloadResponse.data) {
traceSuccess(
Action.NotebooksGalleryDownload,
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
startKey
);
onComplete(increaseDownloadResponse.data);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryDownload,
{
const clearInProgressMessage = logConsoleProgress(`Downloading ${name} to My Notebooks`);
const startKey = traceStart(Action.NotebooksGalleryDownload, {
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
});
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
}
try {
const response = await junoClient.getNotebookContent(data.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
}
clearInProgressMessage();
const notebook = JSON.parse(response.data) as Notebook;
removeNotebookViewerLink(notebook, data.newCellId);
if (!data.isSample) {
const metadata = notebook.metadata as { [name: string]: unknown };
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${name} to My Notebooks`);
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
if (increaseDownloadResponse.data) {
traceSuccess(
Action.NotebooksGalleryDownload,
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
startKey
);
onComplete(increaseDownloadResponse.data);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryDownload,
{
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
}
clearInProgressMessage();
},
"Cancel",
undefined
);
}
export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
if (!newCellId) {
return;

View File

@@ -150,7 +150,7 @@ describe("PricingUtils Tests", () => {
expect(value).toBe(0.00012);
});
it("should return 0.00032 for default cloud, 1RU, 2 region, multimaster enabled", () => {
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
@@ -158,9 +158,9 @@ describe("PricingUtils Tests", () => {
multimasterEnabled: true,
isAutoscale: false,
});
expect(value).toBe(0.00032);
expect(value).toBe(0.00048);
});
it("should return 0.00032 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => {
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => {
const value = PricingUtils.computeRUUsagePriceHourly({
serverId: "default",
requestUnits: 1,
@@ -168,7 +168,7 @@ describe("PricingUtils Tests", () => {
multimasterEnabled: true,
isAutoscale: true,
});
expect(value).toBe(0.00032);
expect(value).toBe(0.00096);
});
});
@@ -251,47 +251,70 @@ describe("PricingUtils Tests", () => {
});
describe("getPricePerRu()", () => {
it("should return 0.00008 for single master default clouds", () => {
const value = PricingUtils.getPricePerRu("default", 1);
it("should return 0.00008 for default clouds", () => {
const value = PricingUtils.getPricePerRu("default");
expect(value).toBe(0.00008);
});
it("should return 0.00016 for multi master default clouds", () => {
const value = PricingUtils.getPricePerRu("default", 2);
expect(value).toBe(0.00016);
});
it("should return 0.00051 for single master mooncake", () => {
const value = PricingUtils.getPricePerRu("mooncake", 1);
it("should return 0.00051 for mooncake", () => {
const value = PricingUtils.getPricePerRu("mooncake");
expect(value).toBe(0.00051);
});
it("should return 0.00102 for multi master mooncake", () => {
const value = PricingUtils.getPricePerRu("mooncake", 2);
expect(value).toBe(0.00102);
});
});
describe("getRegionMultiplier()", () => {
it("should return 0 for undefined", () => {
const value = PricingUtils.getRegionMultiplier(undefined);
expect(value).toBe(0);
describe("without multimaster", () => {
it("should return 0 for undefined", () => {
const value = PricingUtils.getRegionMultiplier(undefined, false);
expect(value).toBe(0);
});
it("should return 0 for -1", () => {
const value = PricingUtils.getRegionMultiplier(-1, false);
expect(value).toBe(0);
});
it("should return 0 for 0", () => {
const value = PricingUtils.getRegionMultiplier(0, false);
expect(value).toBe(0);
});
it("should return 1 for 1", () => {
const value = PricingUtils.getRegionMultiplier(1, false);
expect(value).toBe(1);
});
it("should return 2 for 2", () => {
const value = PricingUtils.getRegionMultiplier(2, false);
expect(value).toBe(2);
});
});
it("should return 0 for -1", () => {
const value = PricingUtils.getRegionMultiplier(-1);
expect(value).toBe(0);
});
it("should return 0 for 0", () => {
const value = PricingUtils.getRegionMultiplier(0);
expect(value).toBe(0);
});
it("should return 1 for 1", () => {
const value = PricingUtils.getRegionMultiplier(1);
expect(value).toBe(1);
});
it("should return 2 for 2", () => {
const value = PricingUtils.getRegionMultiplier(2);
expect(value).toBe(2);
describe("with multimaster", () => {
it("should return 0 for undefined", () => {
const value = PricingUtils.getRegionMultiplier(undefined, true);
expect(value).toBe(0);
});
it("should return 0 for -1", () => {
const value = PricingUtils.getRegionMultiplier(-1, true);
expect(value).toBe(0);
});
it("should return 0 for 0", () => {
const value = PricingUtils.getRegionMultiplier(0, true);
expect(value).toBe(0);
});
it("should return 1 for 1", () => {
const value = PricingUtils.getRegionMultiplier(1, true);
expect(value).toBe(1);
});
it("should return 3 for 2", () => {
const value = PricingUtils.getRegionMultiplier(2, true);
expect(value).toBe(3);
});
});
});
@@ -353,7 +376,7 @@ describe("PricingUtils Tests", () => {
true /* multimaster */
);
expect(value).toBe(
"Cost (USD): <b>$0.13 hourly / $3.07 daily / $93.44 monthly </b> (2 regions, 400RU/s, $0.00016/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
"Cost (USD): <b>$0.19 hourly / $4.61 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
);
});
@@ -401,7 +424,7 @@ describe("PricingUtils Tests", () => {
true /* multimaster */,
false
);
expect(value).toBe("I acknowledge the estimated $3.07 daily cost for the throughput above.");
expect(value).toBe("I acknowledge the estimated $4.61 daily cost for the throughput above.");
});
it("should return 'I acknowledge the estimated $1.54 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, without multimaster", () => {

View File

@@ -34,18 +34,26 @@ export function getRuToolTipText(): string {
* Otherwise, return numberOfRegions
* @param numberOfRegions
*/
export function getRegionMultiplier(numberOfRegions: number): number {
export function getRegionMultiplier(numberOfRegions: number, multimasterEnabled: boolean): number {
const normalizedNumberOfRegions: number = normalizeNumber(numberOfRegions);
if (normalizedNumberOfRegions <= 0) {
return 0;
}
if (numberOfRegions === 1) {
return numberOfRegions;
}
if (multimasterEnabled) {
return numberOfRegions + 1;
}
return numberOfRegions;
}
export function getMultimasterMultiplier(numberOfRegions: number, multimasterEnabled: boolean): number {
const regionMultiplier: number = getRegionMultiplier(numberOfRegions);
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
const multimasterMultiplier: number = !multimasterEnabled ? 1 : regionMultiplier > 1 ? 2 : 1;
return multimasterMultiplier;
@@ -58,12 +66,10 @@ export function computeRUUsagePriceHourly({
multimasterEnabled,
isAutoscale,
}: ComputeRUUsagePriceHourlyArgs): number {
const regionMultiplier: number = getRegionMultiplier(numberOfRegions);
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale
? getAutoscalePricePerRu(serverId, multimasterMultiplier)
: getPricePerRu(serverId, multimasterMultiplier);
const ruCharge = requestUnits * pricePerRu * regionMultiplier;
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId);
const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier;
return Number(ruCharge.toFixed(5));
}
@@ -143,16 +149,12 @@ export function getAutoscalePricePerRu(serverId: string, mmMultiplier: number):
}
}
export function getPricePerRu(serverId: string, mmMultiplier: number): number {
export function getPricePerRu(serverId: string): number {
if (serverId === "mooncake") {
return mmMultiplier > 1
? Constants.OfferPricing.HourlyPricing.mooncake.Standard.MultiMasterPricePerRU
: Constants.OfferPricing.HourlyPricing.mooncake.Standard.SingleMasterPricePerRU;
return Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU;
}
return mmMultiplier > 1
? Constants.OfferPricing.HourlyPricing.default.Standard.MultiMasterPricePerRU
: Constants.OfferPricing.HourlyPricing.default.Standard.SingleMasterPricePerRU;
return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU;
}
export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean): string {
@@ -186,7 +188,9 @@ export function getEstimatedAutoscaleSpendHtml(
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId);
const pricePerRu = getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster));
const pricePerRu =
getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) *
getMultimasterMultiplier(regions, multimaster);
return (
`Estimated monthly cost (${currency}): <b>` +
@@ -215,7 +219,7 @@ export function getEstimatedSpendHtml(
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId);
const pricePerRu = getPricePerRu(serverId, getMultimasterMultiplier(regions, multimaster));
const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster);
return (
`Cost (${currency}): <b>` +

View File

@@ -56,7 +56,6 @@ export function useAADAuth(): ReturnType {
});
setTenantId(response.tenantId);
setAccount(response.account);
localStorage.setItem("cachedTenantId", response.tenantId);
},
[account, tenantId]
);

Some files were not shown because too many files have changed in this diff Show More