mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-05-02 14:33:52 +01:00
Merge branch 'master'
This commit is contained in:
commit
7d905159c6
@ -1,16 +1,8 @@
|
||||
<?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="#CCC"
|
||||
>
|
||||
<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>
|
||||
</svg>
|
Before Width: | Height: | Size: 503 B After Width: | Height: | Size: 449 B |
@ -37,8 +37,8 @@ module.exports = {
|
||||
global: {
|
||||
branches: 25,
|
||||
functions: 25,
|
||||
lines: 30,
|
||||
statements: 30,
|
||||
lines: 29.5,
|
||||
statements: 29.5,
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -2357,6 +2357,8 @@ a:link {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
min-height: 300px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@ -2832,6 +2834,8 @@ a:link {
|
||||
|
||||
#explorerNotificationConsole {
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.uniqueIndexesContainer {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import { userContext } from "../UserContext";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface CollapsedResourceTreeProps {
|
||||
toggleLeftPaneExpanded: () => void;
|
||||
@ -11,6 +12,21 @@ 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">
|
||||
@ -21,11 +37,14 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Expand Tree"
|
||||
onClick={toggleLeftPaneExpanded}
|
||||
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||
ref={focusButton}
|
||||
>
|
||||
<span className="leftarrowCollapsed" onClick={toggleLeftPaneExpanded}>
|
||||
<span className="leftarrowCollapsed">
|
||||
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
||||
</span>
|
||||
<span className="collectionCollapsed" onClick={toggleLeftPaneExpanded}>
|
||||
<span className="collectionCollapsed">
|
||||
<span>{userContext.apiType} API</span>
|
||||
</span>
|
||||
</li>
|
||||
|
@ -96,6 +96,7 @@ 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 {
|
||||
@ -337,6 +338,12 @@ export enum ConflictOperationType {
|
||||
Delete = "delete",
|
||||
}
|
||||
|
||||
export enum ConnectionStatusType {
|
||||
Connecting = "Connecting",
|
||||
Connected = "Connected",
|
||||
Failed = "Connection Failed",
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||
|
@ -3,8 +3,16 @@ 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, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
import {
|
||||
deleteDocument,
|
||||
getEndpoint,
|
||||
getFeatureEndpointOrDefault,
|
||||
queryDocuments,
|
||||
readDocument,
|
||||
updateDocument,
|
||||
} from "./MongoProxyClient";
|
||||
|
||||
const databaseId = "testDB";
|
||||
|
||||
@ -246,4 +254,31 @@ 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ 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";
|
||||
@ -78,7 +79,7 @@ export function queryDocuments(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint() || "";
|
||||
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@ -141,7 +142,8 @@ export function readDocument(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "GET",
|
||||
@ -181,7 +183,7 @@ export function createDocument(
|
||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||
@ -225,7 +227,7 @@ export function updateDocument(
|
||||
? documentId.partitionKeyProperty
|
||||
: "",
|
||||
};
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
@ -266,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
? documentId.partitionKeyProperty
|
||||
: "",
|
||||
};
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
@ -309,7 +311,7 @@ export function createMongoCollectionWithProxy(
|
||||
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||
|
||||
return window
|
||||
.fetch(
|
||||
@ -333,8 +335,15 @@ export function createMongoCollectionWithProxy(
|
||||
});
|
||||
}
|
||||
|
||||
export function getEndpoint(): string {
|
||||
let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer";
|
||||
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";
|
||||
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||
import { AuthType } from "../AuthType";
|
||||
@ -6,6 +6,7 @@ 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;
|
||||
@ -18,6 +19,22 @@ 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 */}
|
||||
@ -43,9 +60,11 @@ 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>
|
||||
|
@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
|
||||
return (
|
||||
<span>
|
||||
<TooltipHost content={children}>
|
||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</span>
|
||||
);
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ConnectionStatusType } from "../Common/Constants";
|
||||
|
||||
export interface DatabaseAccount {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -496,3 +498,8 @@ export interface MemoryUsageInfo {
|
||||
freeKB: number;
|
||||
totalKB: number;
|
||||
}
|
||||
|
||||
export interface ContainerConnectionInfo {
|
||||
status: ConnectionStatusType;
|
||||
//need to add ram and rom info
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
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 {
|
||||
@ -30,6 +31,13 @@ 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 (
|
||||
<>
|
||||
@ -39,6 +47,11 @@ 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>
|
||||
|
@ -3,9 +3,14 @@
|
||||
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,
|
||||
|
@ -181,8 +181,7 @@ export const Dialog: FC = () => {
|
||||
text: secondaryButtonText,
|
||||
onClick: onSecondaryButtonClick,
|
||||
}
|
||||
: {};
|
||||
|
||||
: undefined;
|
||||
return visible ? (
|
||||
<FluentDialog {...dialogProps}>
|
||||
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
||||
|
@ -8,6 +8,9 @@
|
||||
.input-type-head-text-field {
|
||||
width: 100%;
|
||||
}
|
||||
.input-query-form {
|
||||
width: 100%;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
|
@ -160,18 +160,21 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
return (
|
||||
<div className="input-typeahead-container">
|
||||
<Stack horizontal>
|
||||
<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)}
|
||||
/>
|
||||
<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>
|
||||
{this.props.showCancelButton && (
|
||||
<IconButton
|
||||
styles={iconButtonStyles}
|
||||
|
@ -7,16 +7,22 @@ exports[`inputTypeahead renders <input /> 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
multiline={false}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
<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>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
@ -28,16 +34,22 @@ exports[`inputTypeahead renders <textarea /> 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
<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>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
|
@ -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 {
|
||||
ITextFieldStyles,
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
DetailsRow,
|
||||
ICheckboxStyles,
|
||||
IStackProps,
|
||||
IStackTokens,
|
||||
IChoiceGroupStyles,
|
||||
Link,
|
||||
Text,
|
||||
IMessageBarStyles,
|
||||
ITextStyles,
|
||||
IDetailsRowStyles,
|
||||
IStackStyles,
|
||||
IColumn,
|
||||
IDetailsColumnStyles,
|
||||
IDetailsListStyles,
|
||||
IDetailsRowProps,
|
||||
IDetailsRowStyles,
|
||||
IDropdownStyles,
|
||||
IMessageBarStyles,
|
||||
ISeparatorStyles,
|
||||
IStackProps,
|
||||
IStackStyles,
|
||||
IStackTokens,
|
||||
ITextFieldStyles,
|
||||
ITextStyles,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Stack,
|
||||
SelectionMode,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
DetailsList,
|
||||
IColumn,
|
||||
SelectionMode,
|
||||
DetailsListLayoutMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IDetailsColumnStyles,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
||||
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";
|
||||
|
||||
export interface EstimatedSpendingDisplayProps {
|
||||
costType: JSX.Element;
|
||||
@ -65,7 +65,7 @@ export interface PriceBreakdown {
|
||||
currencySign: string;
|
||||
}
|
||||
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||
|
||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||
label: {
|
||||
@ -223,14 +223,15 @@ export const getRuPriceBreakdown = (
|
||||
multimasterEnabled: isMultimaster,
|
||||
isAutoscale: isAutoscale,
|
||||
});
|
||||
const basePricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
|
||||
: getPricePerRu(serverId);
|
||||
const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster);
|
||||
const pricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, multimasterMultiplier)
|
||||
: getPricePerRu(serverId, multimasterMultiplier);
|
||||
return {
|
||||
hourlyPrice: hourlyPrice,
|
||||
hourlyPrice,
|
||||
dailyPrice: hourlyPrice * 24,
|
||||
monthlyPrice: hourlyPrice * hoursInAMonth,
|
||||
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
|
||||
pricePerRu,
|
||||
currency: getPriceCurrency(serverId),
|
||||
currencySign: getCurrencySign(serverId),
|
||||
};
|
||||
@ -271,7 +272,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.{" "}
|
||||
<a href={Urls.autoscaleMigration}>Learn more</a>
|
||||
<Link href={Urls.autoscaleMigration}>Learn more</Link>
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
@ -8,6 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -39,6 +40,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -73,6 +75,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -80,11 +83,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.
|
||||
|
||||
<a
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-migration"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</StyledMessageBar>
|
||||
<StyledChoiceGroup
|
||||
@ -186,6 +189,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -460,6 +464,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
@ -136,6 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -412,6 +413,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -952,6 +954,7 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -1228,6 +1231,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@ -101,6 +102,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
|
@ -159,6 +159,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -166,16 +167,17 @@ 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.
|
||||
|
||||
<a
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-migration"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -195,6 +197,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -207,6 +210,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -219,6 +223,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -230,6 +235,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -249,6 +255,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -264,6 +271,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -278,6 +286,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -291,6 +300,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -302,6 +312,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -321,6 +332,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -363,6 +375,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -378,6 +391,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@ -394,6 +408,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.tabComponentContainer {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
@ -6,6 +6,7 @@ import { userContext } from "../../../../UserContext";
|
||||
import {
|
||||
calculateEstimateNumber,
|
||||
computeRUUsagePriceHourly,
|
||||
estimatedCostDisclaimer,
|
||||
getAutoscalePricePerRu,
|
||||
getCurrencySign,
|
||||
getMultimasterMultiplier,
|
||||
@ -42,11 +43,9 @@ 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) * multiplier
|
||||
: getPricePerRu(serverId) * multiplier;
|
||||
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
|
||||
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>;
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
|
||||
|
||||
if (isAutoscale) {
|
||||
return (
|
||||
|
@ -118,6 +118,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
<input
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Autoscale mode"
|
||||
aria-required={true}
|
||||
checked={isAutoscaleSelected}
|
||||
type="radio"
|
||||
role="radio"
|
||||
@ -131,6 +132,7 @@ 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")}
|
||||
|
@ -345,12 +345,14 @@ 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,
|
||||
@ -630,6 +632,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className="panelInfoIcon root-57"
|
||||
data-icon-name="Info"
|
||||
role="img"
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
</i>
|
||||
@ -651,6 +654,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-label="Autoscale mode"
|
||||
aria-required={true}
|
||||
checked={true}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.0"
|
||||
@ -667,6 +671,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
</span>
|
||||
<input
|
||||
aria-label="Manual mode"
|
||||
aria-required={true}
|
||||
checked={false}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.2"
|
||||
@ -1327,12 +1332,14 @@ 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,
|
||||
@ -1612,6 +1619,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className="panelInfoIcon root-57"
|
||||
data-icon-name="Info"
|
||||
role="img"
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
</i>
|
||||
|
@ -243,6 +243,7 @@ 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={{
|
||||
|
@ -211,6 +211,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
},
|
||||
}
|
||||
}
|
||||
title="More"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -423,6 +424,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
},
|
||||
}
|
||||
}
|
||||
title="More"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,6 +16,7 @@ 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";
|
||||
@ -87,12 +88,13 @@ 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
|
||||
@ -343,19 +345,36 @@ export default class Explorer {
|
||||
return;
|
||||
}
|
||||
this._isInitializingNotebooks = true;
|
||||
if (userContext.features.phoenix) {
|
||||
const provisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
resourceId: userContext.databaseAccount.id,
|
||||
dbAccountName: userContext.databaseAccount.name,
|
||||
aadToken: userContext.authorizationToken,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
};
|
||||
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
|
||||
if (connectionInfo.data && connectionInfo.data.notebookServerUrl) {
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await this.ensureNotebookWorkspaceRunning();
|
||||
const connectionInfo = await listConnectionInfo(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
|
||||
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().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
||||
});
|
||||
}
|
||||
|
||||
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
||||
|
||||
@ -364,7 +383,7 @@ export default class Explorer {
|
||||
this._isInitializingNotebooks = false;
|
||||
}
|
||||
|
||||
public resetNotebookWorkspace() {
|
||||
public resetNotebookWorkspace(): void {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
||||
handleError(
|
||||
"Attempt to reset notebook workspace, but notebook is not enabled",
|
||||
@ -389,7 +408,6 @@ export default class Explorer {
|
||||
if (!databaseAccount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { value: workspaces } = await listByDatabaseAccount(
|
||||
userContext.subscriptionId,
|
||||
@ -906,7 +924,7 @@ export default class Explorer {
|
||||
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
||||
}
|
||||
|
||||
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
|
||||
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||
let title: string;
|
||||
|
||||
switch (kind) {
|
||||
@ -1026,7 +1044,10 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async handleOpenFileAction(path: string): Promise<void> {
|
||||
if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
|
||||
if (
|
||||
userContext.features.phoenix === false &&
|
||||
!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))
|
||||
) {
|
||||
this._openSetupNotebooksPaneForQuickstart();
|
||||
}
|
||||
|
||||
@ -1072,10 +1093,13 @@ export default class Explorer {
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
const isNotebookEnabled: boolean =
|
||||
userContext.authType !== AuthType.ResourceToken &&
|
||||
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
|
||||
userContext.features.enableNotebooks);
|
||||
let isNotebookEnabled = true;
|
||||
if (!userContext.features.phoenix) {
|
||||
isNotebookEnabled =
|
||||
userContext.authType !== AuthType.ResourceToken &&
|
||||
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
|
||||
userContext.features.enableNotebooks);
|
||||
}
|
||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
|
||||
|
||||
|
@ -736,7 +736,7 @@ export class D3ForceGraph implements GraphRenderer {
|
||||
.on("dblclick", function (this: Element, _: MouseEvent, d: D3Node) {
|
||||
// https://stackoverflow.com/a/41945742 ('this' implicitly has type 'any' because it does not have a type annotation)
|
||||
// this is the <g> element
|
||||
self.onNodeClicked(this.parentNode, d);
|
||||
return self.onNodeClicked(this.parentNode, d);
|
||||
})
|
||||
.on("click", function (this: Element, _: MouseEvent, d: D3Node) {
|
||||
// this is the <g> element
|
||||
|
@ -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,7 +17,14 @@ 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}>
|
||||
<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}
|
||||
>
|
||||
<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
@ -9,6 +9,7 @@ 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 { useSelectedNode } from "../../useSelectedNode";
|
||||
@ -54,6 +55,14 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
|
||||
if (
|
||||
userContext.features.notebooksTemporarilyDown === false &&
|
||||
userContext.features.phoenix === true &&
|
||||
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
|
||||
) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus"));
|
||||
}
|
||||
|
||||
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
|
||||
}
|
||||
|
@ -80,8 +80,9 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
|
||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||
if (userContext.features.phoenix === false) {
|
||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||
}
|
||||
if (
|
||||
(userContext.apiType === "Mongo" &&
|
||||
useNotebook.getState().isShellEnabled &&
|
||||
|
@ -13,6 +13,7 @@ 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 { ConnectionStatus } from "./ConnectionStatusComponent";
|
||||
import { MemoryTracker } from "./MemoryTrackerComponent";
|
||||
|
||||
/**
|
||||
@ -201,3 +202,10 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
|
||||
onRender: () => <MemoryTracker />,
|
||||
};
|
||||
};
|
||||
|
||||
export const createConnectionStatus = (key: string): ICommandBarItemProps => {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <ConnectionStatus />,
|
||||
};
|
||||
};
|
||||
|
79
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less
Normal file
79
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less
Normal file
@ -0,0 +1,79 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.connectionStatusContainer {
|
||||
cursor: default;
|
||||
align-items: center;
|
||||
margin: 0 9px;
|
||||
border: 1px;
|
||||
min-height: 44px;
|
||||
|
||||
> span {
|
||||
padding-right: 12px;
|
||||
font-size: 13px;
|
||||
font-family: @DataExplorerFont;
|
||||
color: @DefaultFontColor;
|
||||
}
|
||||
}
|
||||
.connectionStatusFailed{
|
||||
color: #bd1919;
|
||||
}
|
||||
.ring-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ringringGreen {
|
||||
border: 3px solid green;
|
||||
border-radius: 30px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
position: absolute;
|
||||
margin: .4285em 0em 0em 0.07477em;
|
||||
animation: pulsate 3s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
opacity: 0.0
|
||||
}
|
||||
.ringringYellow{
|
||||
border: 3px solid #ffbf00;
|
||||
border-radius: 30px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
position: absolute;
|
||||
margin: .4285em 0em 0em 0.07477em;
|
||||
animation: pulsate 3s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
opacity: 0.0
|
||||
}
|
||||
.ringringRed{
|
||||
border: 3px solid #bd1919;
|
||||
border-radius: 30px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
position: absolute;
|
||||
margin: .4285em 0em 0em 0.07477em;
|
||||
animation: pulsate 3s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
opacity: 0.0
|
||||
}
|
||||
@keyframes pulsate {
|
||||
0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;}
|
||||
15% {opacity: 0.8;}
|
||||
25% {opacity: 0.6;}
|
||||
45% {opacity: 0.4;}
|
||||
70% {opacity: 0.3;}
|
||||
100% {-webkit-transform: scale(.7, .7); opacity: 0.1;}
|
||||
}
|
||||
.locationGreenDot{
|
||||
font-size: 20px;
|
||||
margin-right: 0.07em;
|
||||
color: green;
|
||||
}
|
||||
.locationYellowDot{
|
||||
font-size: 20px;
|
||||
margin-right: 0.07em;
|
||||
color: #ffbf00;
|
||||
}
|
||||
.locationRedDot{
|
||||
font-size: 20px;
|
||||
margin-right: 0.07em;
|
||||
color: #bd1919;
|
||||
}
|
72
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx
Normal file
72
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { ConnectionStatusType } from "../../../Common/Constants";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import "../CommandBar/ConnectionStatusComponent.less";
|
||||
|
||||
export const ConnectionStatus: React.FC = (): 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("locationYellowDot");
|
||||
const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow");
|
||||
const toolTipContent = "Hosted runtime status.";
|
||||
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);
|
||||
if (!connectionInfo) {
|
||||
return <></>;
|
||||
}
|
||||
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
|
||||
setIsActive(true);
|
||||
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
|
||||
stopTimer();
|
||||
setStatusColor("locationGreenDot");
|
||||
setStatusColorAnimation("ringringGreen");
|
||||
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
|
||||
stopTimer();
|
||||
setStatusColor("locationRedDot");
|
||||
setStatusColorAnimation("ringringRed");
|
||||
}
|
||||
return (
|
||||
<TooltipHost content={toolTipContent}>
|
||||
<Stack className="connectionStatusContainer" horizontal>
|
||||
<div className="ring-container">
|
||||
<div className={statusColorAnimation}></div>
|
||||
<Icon iconName="LocationDot" className={statusColor} />
|
||||
</div>
|
||||
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
|
||||
{connectionInfo.status}
|
||||
</span>
|
||||
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
|
||||
<ProgressIndicator description={minute + ":" + second} />
|
||||
)}
|
||||
</Stack>
|
||||
</TooltipHost>
|
||||
);
|
||||
};
|
@ -129,7 +129,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " collapsed" : " expanded")}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||
aria-expanded={!this.props.isConsoleExpanded}
|
||||
>
|
||||
<img
|
||||
@ -205,7 +205,9 @@ 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">{item.message}</span>
|
||||
<span className="message" role="alert" aria-live="assertive">
|
||||
{item.message}
|
||||
</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button expanded"
|
||||
aria-label="console button collapsed"
|
||||
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 expanded"
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@ -340,7 +340,9 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
date
|
||||
</span>
|
||||
<span
|
||||
aria-live="assertive"
|
||||
className="message"
|
||||
role="alert"
|
||||
>
|
||||
message
|
||||
</span>
|
||||
|
@ -109,7 +109,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
|
||||
const q = params.toString();
|
||||
const suffix = q !== "" ? `?${q}` : "";
|
||||
|
||||
const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
||||
const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
||||
|
||||
return url.replace(/^http(s)?/, "ws$1");
|
||||
};
|
||||
|
@ -56,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,
|
||||
|
@ -36,7 +36,10 @@ export class NotebookContentClient {
|
||||
*
|
||||
* @param parent parent folder
|
||||
*/
|
||||
public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise<NotebookContentItem> {
|
||||
public async createNewNotebookFile(
|
||||
parent: NotebookContentItem,
|
||||
isGithubTree?: boolean
|
||||
): Promise<NotebookContentItem> {
|
||||
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
||||
throw new Error(`Parent must be a directory: ${parent}`);
|
||||
}
|
||||
@ -99,7 +102,6 @@ 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}`);
|
||||
|
@ -28,6 +28,8 @@ interface NotebookState {
|
||||
myNotebooksContentRoot: NotebookContentItem;
|
||||
gitHubNotebooksContentRoot: NotebookContentItem;
|
||||
galleryContentRoot: NotebookContentItem;
|
||||
connectionInfo: DataModels.ContainerConnectionInfo;
|
||||
notebookFolderName: string;
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||
@ -36,6 +38,7 @@ 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;
|
||||
@ -43,6 +46,7 @@ interface NotebookState {
|
||||
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
|
||||
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
|
||||
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void;
|
||||
}
|
||||
|
||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
@ -65,6 +69,8 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
myNotebooksContentRoot: undefined,
|
||||
gitHubNotebooksContentRoot: undefined,
|
||||
galleryContentRoot: undefined,
|
||||
connectionInfo: undefined,
|
||||
notebookFolderName: undefined,
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||
@ -75,6 +81,7 @@ 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 (
|
||||
@ -168,8 +175,10 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||
const notebookFolderName = userContext.features.phoenix === true ? "Temporary Notebooks" : "My Notebooks";
|
||||
set({ notebookFolderName });
|
||||
const myNotebooksContentRoot = {
|
||||
name: "My Notebooks",
|
||||
name: get().notebookFolderName,
|
||||
path: get().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
@ -185,6 +194,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
type: NotebookContentItemType.Directory,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
set({
|
||||
myNotebooksContentRoot,
|
||||
galleryContentRoot,
|
||||
@ -246,4 +256,5 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
set({ gitHubNotebooksContentRoot });
|
||||
}
|
||||
},
|
||||
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }),
|
||||
}));
|
||||
|
@ -161,7 +161,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
true
|
||||
).toLocaleLowerCase()}.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@ -210,6 +210,7 @@ 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 })
|
||||
@ -236,7 +237,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
true
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
@ -279,7 +280,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" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@ -362,7 +363,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" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@ -409,7 +410,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getPartitionKeyTooltipText()}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@ -467,7 +468,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" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
@ -497,7 +498,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" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@ -560,7 +561,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getAnalyticalStorageTooltipContent()}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@ -637,7 +638,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" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
|
@ -23,10 +23,12 @@ 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;
|
||||
@ -77,6 +79,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
};
|
||||
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
|
||||
buttonElement.focus();
|
||||
}, []);
|
||||
|
||||
const onSubmit = () => {
|
||||
|
@ -198,6 +198,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
<Stack className="panelGroupSpacing">
|
||||
<TextField
|
||||
aria-required="true"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
styles={getTextFieldStyles()}
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
@ -285,6 +286,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
underlined
|
||||
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
||||
aria-required="true"
|
||||
required={true}
|
||||
ariaLabel="addCollection-tableId"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
|
@ -5,6 +5,7 @@ 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";
|
||||
@ -75,6 +76,8 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
selectedLocation.owner,
|
||||
selectedLocation.repo
|
||||
)} - ${selectedLocation.branch}`;
|
||||
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
|
||||
destination = "My Notebooks Scratch";
|
||||
}
|
||||
|
||||
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
|
||||
|
@ -12,6 +12,7 @@ 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 {
|
||||
@ -46,11 +47,10 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
||||
|
||||
const getDropDownOptions = (): IDropdownOption[] => {
|
||||
const options: IDropdownOption[] = [];
|
||||
|
||||
options.push({
|
||||
key: "MyNotebooks-Item",
|
||||
text: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
title: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
text: useNotebook.getState().notebookFolderName,
|
||||
title: useNotebook.getState().notebookFolderName,
|
||||
data: {
|
||||
type: "MyNotebooks",
|
||||
} as Location,
|
||||
|
@ -108,6 +108,8 @@ 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">
|
||||
@ -123,6 +125,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
onChange={(event, newInput?: string) => {
|
||||
setInputCollectionName(newInput);
|
||||
}}
|
||||
ariaLabel={confirmContainer}
|
||||
/>
|
||||
</div>
|
||||
{shouldRecordFeedback() && (
|
||||
@ -142,6 +145,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDeleteCollectionFeedback(newInput);
|
||||
}}
|
||||
ariaLabel={reasonInfo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -40,6 +40,7 @@ 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]}
|
||||
@ -53,6 +54,7 @@ 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"
|
||||
@ -346,6 +348,7 @@ 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"
|
||||
|
@ -118,7 +118,8 @@ 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} />}
|
||||
@ -133,6 +134,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDatabaseInput(newInput);
|
||||
}}
|
||||
ariaLabel={confirmDatabase}
|
||||
/>
|
||||
</div>
|
||||
{isLastNonEmptyDatabase() && (
|
||||
@ -151,6 +153,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDatabaseFeedbackInput(newInput);
|
||||
}}
|
||||
ariaLabel={reasonInfo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -153,7 +153,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
selectedKey={paramKeyValue && paramKeyValue.key}
|
||||
/>
|
||||
))}
|
||||
<Stack horizontal onClick={addNewParamAtLastIndex}>
|
||||
<Stack horizontal onClick={addNewParamAtLastIndex} tabIndex={0}>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
||||
<Text className="addNewParamStyle">Add New Param</Text>
|
||||
</Stack>
|
||||
|
@ -59,30 +59,36 @@ 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 && (
|
||||
<>
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={EntityCancelIcon}
|
||||
alt="Delete param"
|
||||
id="deleteparam"
|
||||
onClick={onDeleteParamKeyPress}
|
||||
/>
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={AddPropertyIcon}
|
||||
alt="Add param"
|
||||
id="addparam"
|
||||
onClick={onAddNewParamKeyPress}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
|
@ -3,13 +3,13 @@
|
||||
.panelFormWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
.panelMainContent {
|
||||
flex-grow: 1;
|
||||
padding: 0 34px;
|
||||
margin: 20px 0;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
& > :not(.collapsibleSection) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
|
@ -33,7 +33,13 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
||||
{icon}
|
||||
<span className="panelWarningErrorDetailsLinkContainer">
|
||||
<Text className="panelWarningErrorMessage" variant="small" aria-label="message">
|
||||
<Text
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-label={message}
|
||||
className="panelWarningErrorMessage"
|
||||
variant="small"
|
||||
>
|
||||
{message}
|
||||
{link && linkText && (
|
||||
<Link target="_blank" href={link}>
|
||||
|
@ -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.getByLabelText("message").innerHTML).toEqual("file already Exist");
|
||||
expect(screen.getByRole("alert").innerHTML).toEqual("file already Exist");
|
||||
});
|
||||
});
|
||||
|
@ -195,7 +195,6 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
step={1}
|
||||
className="textfontclr"
|
||||
role="textbox"
|
||||
tabIndex={0}
|
||||
id="max-degree"
|
||||
value={"" + maxDegreeOfParallelism}
|
||||
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
|
||||
|
@ -123,7 +123,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
onValidate={[Function]}
|
||||
role="textbox"
|
||||
step={1}
|
||||
tabIndex={0}
|
||||
value="6"
|
||||
/>
|
||||
</div>
|
||||
|
@ -13,6 +13,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
|
@ -36,7 +36,6 @@ import {
|
||||
interface AddTableEntityPanelProps {
|
||||
tableDataClient: TableDataClient;
|
||||
queryTablesTab: NewQueryTablesTab;
|
||||
// queryTablesTab: QueryTablesTab;
|
||||
tableEntityListViewModel: TableEntityListViewModel;
|
||||
cassandraApiClient: CassandraAPIDataClient;
|
||||
reloadEntities: () => void;
|
||||
|
@ -27,6 +27,7 @@ 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,
|
||||
|
@ -134,8 +134,9 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
||||
label="Available Columns"
|
||||
checked={isAvailableColumnChecked}
|
||||
onChange={availableColumnsCheckboxClick}
|
||||
ariaPositionInSet={0}
|
||||
/>
|
||||
{columnOptions.map((column) => {
|
||||
{columnOptions.map((column, index) => {
|
||||
return (
|
||||
<Checkbox
|
||||
label={column.columnName}
|
||||
@ -143,6 +144,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
||||
key={column.columnName}
|
||||
checked={column.selected}
|
||||
disabled={!column.editable}
|
||||
ariaPositionInSet={index + 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -41,12 +41,14 @@ 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"
|
||||
@ -332,6 +334,7 @@ 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}
|
||||
@ -650,6 +653,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
</CheckboxBase>
|
||||
</StyledCheckboxBase>
|
||||
<StyledCheckboxBase
|
||||
ariaPositionInSet={1}
|
||||
checked={true}
|
||||
disabled={false}
|
||||
key=""
|
||||
@ -657,6 +661,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
onChange={[Function]}
|
||||
>
|
||||
<CheckboxBase
|
||||
ariaPositionInSet={1}
|
||||
checked={true}
|
||||
disabled={false}
|
||||
label=""
|
||||
@ -943,6 +948,7 @@ 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}
|
||||
|
@ -327,13 +327,17 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
key=".0:$.1"
|
||||
>
|
||||
<Text
|
||||
aria-label="message"
|
||||
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"
|
||||
className="panelWarningErrorMessage"
|
||||
role="alert"
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
aria-label="message"
|
||||
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"
|
||||
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>
|
||||
@ -363,6 +367,7 @@ 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]}
|
||||
@ -375,6 +380,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
}
|
||||
>
|
||||
<TextFieldBase
|
||||
ariaLabel="Confirm by typing the database id"
|
||||
autoFocus={true}
|
||||
deferredValidationTime={200}
|
||||
id="confirmDatabaseId"
|
||||
@ -667,6 +673,7 @@ 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"
|
||||
@ -707,6 +714,7 @@ 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]}
|
||||
@ -720,6 +728,7 @@ 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}
|
||||
@ -1013,6 +1022,7 @@ 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]}
|
||||
|
@ -307,16 +307,23 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
iconSrc: AddDatabaseIcon,
|
||||
title: "New " + getDatabaseName(),
|
||||
description: undefined,
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this.container} />),
|
||||
onClick: () => this.openAddDatabasePanel(),
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode } from "@fluentui/react";
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode, Text } from "@fluentui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import SplitterLayout from "react-splitter-layout";
|
||||
import "react-splitter-layout/lib/index.css";
|
||||
@ -120,21 +120,13 @@ 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",
|
||||
@ -206,7 +198,12 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
|
||||
public onRenderColumnItem(item: IDocument): JSX.Element {
|
||||
if (item.toolTip !== "") {
|
||||
return <InfoTooltip>{`${item.toolTip}`}</InfoTooltip>;
|
||||
return (
|
||||
<>
|
||||
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
|
||||
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
@ -1002,7 +999,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
"data-order": 2,
|
||||
"data-title": "Query Stats",
|
||||
}}
|
||||
style={{ height: "100%" }}
|
||||
style={{ height: "100%", overflowY: "scroll" }}
|
||||
>
|
||||
{this.state.allResultsMetadata.length > 0 && !this.state.error && (
|
||||
<div className="queryMetricsSummaryContainer">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import ko from "knockout";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { MutableRefObject, 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,7 +32,13 @@ 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)}
|
||||
@ -46,6 +52,7 @@ 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}>
|
||||
|
@ -291,7 +291,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
language={"javascript"}
|
||||
content={udfBody}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Graph JSON"}
|
||||
ariaLabel={"User defined function body"}
|
||||
onContentChanged={this.handleUdfBodyChange}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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, Notebook } from "../../Common/Constants";
|
||||
import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants";
|
||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@ -128,17 +128,15 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
notebooksTree.children.push(buildGalleryNotebooksTree());
|
||||
}
|
||||
|
||||
if (myNotebooksContentRoot) {
|
||||
if (myNotebooksContentRoot && useNotebook.getState().connectionInfo.status == ConnectionStatusType.Connected) {
|
||||
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());
|
||||
notebooksTree.children.push(buildGitHubNotebooksTree(true));
|
||||
}
|
||||
}
|
||||
|
||||
return notebooksTree;
|
||||
};
|
||||
|
||||
@ -178,7 +176,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
return myNotebooksTree;
|
||||
};
|
||||
|
||||
const buildGitHubNotebooksTree = (): TreeNode => {
|
||||
const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => {
|
||||
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
|
||||
gitHubNotebooksContentRoot,
|
||||
(item: NotebookContentItem) => {
|
||||
@ -190,8 +188,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
gitHubNotebooksTree.contextMenu = [
|
||||
const manageGitContextMenu: TreeNodeMenuItem[] = [
|
||||
{
|
||||
label: "Manage GitHub settings",
|
||||
onClick: () =>
|
||||
@ -216,7 +213,23 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const connectGitContextMenu: TreeNodeMenuItem[] = [
|
||||
{
|
||||
label: "Connect to GitHub",
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Connect to GitHub",
|
||||
<GitHubReposPanel
|
||||
explorer={container}
|
||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||
junoClientProp={container.notebookManager.junoClient}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
gitHubNotebooksTree.contextMenu = isConnected ? manageGitContextMenu : connectGitContextMenu;
|
||||
gitHubNotebooksTree.isExpanded = true;
|
||||
gitHubNotebooksTree.isAlphaSorted = true;
|
||||
|
||||
|
@ -45,6 +45,7 @@ import UserDefinedFunction from "./UserDefinedFunction";
|
||||
|
||||
export class ResourceTreeAdapter implements ReactAdapter {
|
||||
public static readonly MyNotebooksTitle = "My Notebooks";
|
||||
public static readonly MyNotebooksScratchTitle = "My Notebooks Scratch";
|
||||
public static readonly GitHubReposTitle = "GitHub repos";
|
||||
|
||||
private static readonly DataTitle = "DATA";
|
||||
@ -130,9 +131,8 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
path: "Gallery",
|
||||
type: NotebookContentItemType.File,
|
||||
};
|
||||
|
||||
this.myNotebooksContentRoot = {
|
||||
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
name: useNotebook.getState().notebookFolderName,
|
||||
path: useNotebook.getState().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
@ -146,16 +146,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||
this.gitHubNotebooksContentRoot = {
|
||||
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||
path: ResourceTreeAdapter.PseudoDirPath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
} else {
|
||||
this.gitHubNotebooksContentRoot = undefined;
|
||||
}
|
||||
this.gitHubNotebooksContentRoot = {
|
||||
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||
path: ResourceTreeAdapter.PseudoDirPath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
|
||||
return Promise.all(refreshTasks);
|
||||
}
|
||||
|
57
src/Localization/en/GraphAPICompute.json
Normal file
57
src/Localization/en/GraphAPICompute.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"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."
|
||||
}
|
@ -37,6 +37,7 @@ 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";
|
||||
|
85
src/Phoenix/PhoenixClient.ts
Normal file
85
src/Phoenix/PhoenixClient.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { ContainerConnectionInfo } from "../Contracts/DataModels";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
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;
|
||||
resourceId: string;
|
||||
dbAccountName: string;
|
||||
aadToken: string;
|
||||
resourceGroup: string;
|
||||
subscriptionId: string;
|
||||
}
|
||||
export class PhoenixClient {
|
||||
public async containerConnectionInfo(
|
||||
provisionData: IProvosionData
|
||||
): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> {
|
||||
try {
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, {
|
||||
method: "POST",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
body: JSON.stringify(provisionData),
|
||||
});
|
||||
let data: IPhoenixConnectionInfoResult;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
if (data && data.notebookServerUrl) {
|
||||
connectionStatus.status = ConnectionStatusType.Connected;
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
}
|
||||
} else {
|
||||
connectionStatus.status = ConnectionStatusType.Failed;
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
} catch (error) {
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Failed,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public static getPhoenixEndpoint(): string {
|
||||
const 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/containerpooling`;
|
||||
}
|
||||
private static getHeaders(): HeadersInit {
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
return {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
};
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { extractFeatures } from "./extractFeatures";
|
||||
import { extractFeatures, hasFlag } from "./extractFeatures";
|
||||
|
||||
describe("extractFeatures", () => {
|
||||
it("correctly detects feature flags in a case insensitive manner", () => {
|
||||
@ -14,9 +14,25 @@ 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);
|
||||
});
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ export type Features = {
|
||||
autoscaleDefault: boolean;
|
||||
partitionKeyDefault: boolean;
|
||||
partitionKeyDefault2: boolean;
|
||||
phoenix: boolean;
|
||||
readonly enableSDKoperations: boolean;
|
||||
readonly enableSpark: boolean;
|
||||
readonly enableTtl: boolean;
|
||||
@ -28,6 +29,8 @@ export type Features = {
|
||||
readonly pr?: string;
|
||||
readonly showMinRUSurvey: boolean;
|
||||
readonly ttl90Days: boolean;
|
||||
readonly mongoProxyEndpoint?: string;
|
||||
readonly mongoProxyAPIs?: string;
|
||||
readonly notebooksTemporarilyDown: boolean;
|
||||
};
|
||||
|
||||
@ -62,6 +65,8 @@ 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"),
|
||||
livyEndpoint: get("livyendpoint"),
|
||||
notebookBasePath: get("notebookbasepath"),
|
||||
@ -76,5 +81,15 @@ 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;
|
||||
}
|
||||
|
200
src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts
Normal file
200
src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts
Normal file
@ -0,0 +1,200 @@
|
||||
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:
|
||||
"armRegionName eq '" +
|
||||
region +
|
||||
"' and serviceFamily eq '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;
|
||||
}
|
||||
};
|
423
src/SelfServe/GraphAPICompute/GraphAPICompute.tsx
Normal file
423
src/SelfServe/GraphAPICompute/GraphAPICompute.tsx
Normal file
@ -0,0 +1,423 @@
|
||||
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;
|
||||
}
|
65
src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts
Normal file
65
src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts
Normal file
@ -0,0 +1,65 @@
|
||||
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;
|
||||
};
|
@ -50,6 +50,14 @@ 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;
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ export enum SelfServeType {
|
||||
// Add your self serve types here
|
||||
example = "example",
|
||||
sqlx = "sqlx",
|
||||
graphapicompute = "graphapicompute",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -138,9 +138,17 @@ const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: str
|
||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
|
||||
};
|
||||
|
||||
export const getReadRegions = async (): Promise<Array<string>> => {
|
||||
export const getRegions = async (): Promise<Array<string>> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getRegions",
|
||||
description: "",
|
||||
selfServeClassName: SqlX.name,
|
||||
};
|
||||
const getRegionsTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
const readRegions = new Array<string>();
|
||||
const regions = new Array<string>();
|
||||
|
||||
const response = await armRequestWithoutPolling<RegionsResponse>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
@ -150,14 +158,18 @@ export const getReadRegions = async (): Promise<Array<string>> => {
|
||||
});
|
||||
|
||||
if (response.result.location !== undefined) {
|
||||
readRegions.push(response.result.location.replace(" ", "").toLowerCase());
|
||||
regions.push(response.result.location.split(" ").join("").toLowerCase());
|
||||
} else {
|
||||
for (const location of response.result.locations) {
|
||||
readRegions.push(location.locationName.replace(" ", "").toLowerCase());
|
||||
regions.push(location.locationName.split(" ").join("").toLowerCase());
|
||||
}
|
||||
}
|
||||
return readRegions;
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getRegionsTimestamp);
|
||||
return regions;
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
|
||||
return new Array<string>();
|
||||
}
|
||||
};
|
||||
@ -167,6 +179,14 @@ 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>>();
|
||||
|
||||
@ -192,8 +212,12 @@ 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;
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
|
||||
import { selfServeTrace } from "../SelfServeTelemetryProcessor";
|
||||
import {
|
||||
selfServeTrace,
|
||||
selfServeTraceFailure,
|
||||
selfServeTraceStart,
|
||||
selfServeTraceSuccess,
|
||||
} from "../SelfServeTelemetryProcessor";
|
||||
import {
|
||||
ChoiceItem,
|
||||
Description,
|
||||
@ -17,7 +22,7 @@ import {
|
||||
deleteDedicatedGatewayResource,
|
||||
getCurrentProvisioningState,
|
||||
getPriceMap,
|
||||
getReadRegions,
|
||||
getRegions,
|
||||
refreshDedicatedGatewayProvisioning,
|
||||
updateDedicatedGatewayResource,
|
||||
} from "./SqlX.rp";
|
||||
@ -205,6 +210,14 @@ 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) {
|
||||
@ -215,14 +228,22 @@ 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;
|
||||
}
|
||||
};
|
||||
@ -324,7 +345,7 @@ export default class SqlX extends SelfServeBaseClass {
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
regions = await getReadRegions();
|
||||
regions = await getRegions();
|
||||
priceMap = await getPriceMap(regions);
|
||||
|
||||
const response = await getCurrentProvisioningState();
|
||||
|
@ -125,7 +125,8 @@ export class OfferPricing {
|
||||
S3Price: 0.1344,
|
||||
Standard: {
|
||||
StartingPrice: 24 / hoursInAMonth, // per hour
|
||||
PricePerRU: 0.00008,
|
||||
SingleMasterPricePerRU: 0.00008,
|
||||
MultiMasterPricePerRU: 0.00016,
|
||||
PricePerGB: 0.25 / hoursInAMonth,
|
||||
},
|
||||
},
|
||||
@ -137,7 +138,8 @@ export class OfferPricing {
|
||||
S3Price: 0.6,
|
||||
Standard: {
|
||||
StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour
|
||||
PricePerRU: 0.00051,
|
||||
SingleMasterPricePerRU: 0.00051,
|
||||
MultiMasterPricePerRU: 0.00102,
|
||||
PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth,
|
||||
},
|
||||
},
|
||||
|
@ -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.PricePerRU;
|
||||
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.SingleMasterPricePerRU;
|
||||
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency;
|
||||
}
|
||||
|
||||
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU;
|
||||
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.SingleMasterPricePerRU;
|
||||
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,3 @@
|
||||
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;
|
||||
|
@ -2,6 +2,7 @@ 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";
|
||||
|
||||
@ -34,7 +35,7 @@ describe("GalleryUtils", () => {
|
||||
|
||||
expect(useDialog.getState().visible).toBe(true);
|
||||
expect(useDialog.getState().dialogProps).toBeDefined();
|
||||
expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks");
|
||||
expect(useDialog.getState().dialogProps.title).toBe(`Download to ${useNotebook.getState().notebookFolderName}`);
|
||||
});
|
||||
|
||||
it("favoriteItem favorites item", async () => {
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
SortBy,
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
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";
|
||||
@ -223,11 +224,13 @@ export function downloadItem(
|
||||
|
||||
const name = data.name;
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
"Download to My Notebooks",
|
||||
`Download to ${useNotebook.getState().notebookFolderName}`,
|
||||
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
|
||||
"Download",
|
||||
async () => {
|
||||
const clearInProgressMessage = logConsoleProgress(`Downloading ${name} to My Notebooks`);
|
||||
const clearInProgressMessage = logConsoleProgress(
|
||||
`Downloading ${name} to ${useNotebook.getState().notebookFolderName}`
|
||||
);
|
||||
const startKey = traceStart(Action.NotebooksGalleryDownload, {
|
||||
notebookId: data.id,
|
||||
downloadCount: data.downloads,
|
||||
|
@ -150,7 +150,7 @@ describe("PricingUtils Tests", () => {
|
||||
expect(value).toBe(0.00012);
|
||||
});
|
||||
|
||||
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => {
|
||||
it("should return 0.00032 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.00048);
|
||||
expect(value).toBe(0.00032);
|
||||
});
|
||||
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => {
|
||||
it("should return 0.00032 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.00096);
|
||||
expect(value).toBe(0.00032);
|
||||
});
|
||||
});
|
||||
|
||||
@ -251,70 +251,47 @@ describe("PricingUtils Tests", () => {
|
||||
});
|
||||
|
||||
describe("getPricePerRu()", () => {
|
||||
it("should return 0.00008 for default clouds", () => {
|
||||
const value = PricingUtils.getPricePerRu("default");
|
||||
it("should return 0.00008 for single master default clouds", () => {
|
||||
const value = PricingUtils.getPricePerRu("default", 1);
|
||||
expect(value).toBe(0.00008);
|
||||
});
|
||||
|
||||
it("should return 0.00051 for mooncake", () => {
|
||||
const value = PricingUtils.getPricePerRu("mooncake");
|
||||
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);
|
||||
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()", () => {
|
||||
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 undefined", () => {
|
||||
const value = PricingUtils.getRegionMultiplier(undefined);
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -376,7 +353,7 @@ describe("PricingUtils Tests", () => {
|
||||
true /* multimaster */
|
||||
);
|
||||
expect(value).toBe(
|
||||
"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>"
|
||||
"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>"
|
||||
);
|
||||
});
|
||||
|
||||
@ -424,7 +401,7 @@ describe("PricingUtils Tests", () => {
|
||||
true /* multimaster */,
|
||||
false
|
||||
);
|
||||
expect(value).toBe("I acknowledge the estimated $4.61 daily cost for the throughput above.");
|
||||
expect(value).toBe("I acknowledge the estimated $3.07 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", () => {
|
||||
|
@ -34,26 +34,18 @@ export function getRuToolTipText(): string {
|
||||
* Otherwise, return numberOfRegions
|
||||
* @param numberOfRegions
|
||||
*/
|
||||
export function getRegionMultiplier(numberOfRegions: number, multimasterEnabled: boolean): number {
|
||||
export function getRegionMultiplier(numberOfRegions: number): 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, multimasterEnabled);
|
||||
const regionMultiplier: number = getRegionMultiplier(numberOfRegions);
|
||||
const multimasterMultiplier: number = !multimasterEnabled ? 1 : regionMultiplier > 1 ? 2 : 1;
|
||||
|
||||
return multimasterMultiplier;
|
||||
@ -66,10 +58,12 @@ export function computeRUUsagePriceHourly({
|
||||
multimasterEnabled,
|
||||
isAutoscale,
|
||||
}: ComputeRUUsagePriceHourlyArgs): number {
|
||||
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const regionMultiplier: number = getRegionMultiplier(numberOfRegions);
|
||||
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId);
|
||||
const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier;
|
||||
const pricePerRu = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, multimasterMultiplier)
|
||||
: getPricePerRu(serverId, multimasterMultiplier);
|
||||
const ruCharge = requestUnits * pricePerRu * regionMultiplier;
|
||||
|
||||
return Number(ruCharge.toFixed(5));
|
||||
}
|
||||
@ -149,12 +143,16 @@ export function getAutoscalePricePerRu(serverId: string, mmMultiplier: number):
|
||||
}
|
||||
}
|
||||
|
||||
export function getPricePerRu(serverId: string): number {
|
||||
export function getPricePerRu(serverId: string, mmMultiplier: number): number {
|
||||
if (serverId === "mooncake") {
|
||||
return Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU;
|
||||
return mmMultiplier > 1
|
||||
? Constants.OfferPricing.HourlyPricing.mooncake.Standard.MultiMasterPricePerRU
|
||||
: Constants.OfferPricing.HourlyPricing.mooncake.Standard.SingleMasterPricePerRU;
|
||||
}
|
||||
|
||||
return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU;
|
||||
return mmMultiplier > 1
|
||||
? Constants.OfferPricing.HourlyPricing.default.Standard.MultiMasterPricePerRU
|
||||
: Constants.OfferPricing.HourlyPricing.default.Standard.SingleMasterPricePerRU;
|
||||
}
|
||||
|
||||
export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean): string {
|
||||
@ -188,9 +186,7 @@ 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)) *
|
||||
getMultimasterMultiplier(regions, multimaster);
|
||||
const pricePerRu = getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster));
|
||||
|
||||
return (
|
||||
`Estimated monthly cost (${currency}): <b>` +
|
||||
@ -219,7 +215,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>` +
|
||||
|
@ -56,6 +56,7 @@ export function useAADAuth(): ReturnType {
|
||||
});
|
||||
setTenantId(response.tenantId);
|
||||
setAccount(response.account);
|
||||
localStorage.setItem("cachedTenantId", response.tenantId);
|
||||
},
|
||||
[account, tenantId]
|
||||
);
|
||||
|
@ -98,9 +98,11 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
||||
const msalInstance = getMsalInstance();
|
||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
||||
msalInstance.setActiveAccount(cachedAccount);
|
||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||
const aadTokenResponse = await msalInstance.acquireTokenSilent({
|
||||
forceRefresh: true,
|
||||
scopes: [hrefEndpoint],
|
||||
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
|
||||
});
|
||||
aadToken = aadTokenResponse.accessToken;
|
||||
}
|
||||
@ -337,6 +339,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
|
||||
userContext.features.partitionKeyDefault2 = true;
|
||||
}
|
||||
if (inputs.flights.indexOf(Flights.Phoenix) !== -1) {
|
||||
userContext.features.phoenix = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,7 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
envVars.NODE_ENV = "development";
|
||||
envVars.AZURE_CLIENT_ID = AZURE_CLIENT_ID;
|
||||
envVars.AZURE_TENANT_ID = AZURE_TENANT_ID;
|
||||
envVars.AZURE_CLIENT_SECRET = AZURE_CLIENT_SECRET;
|
||||
envVars.AZURE_CLIENT_SECRET = AZURE_CLIENT_SECRET || null;
|
||||
envVars.SUBSCRIPTION_ID = SUBSCRIPTION_ID;
|
||||
envVars.RESOURCE_GROUP = RESOURCE_GROUP;
|
||||
typescriptRule.use[0].options.compilerOptions = { target: "ES2018" };
|
||||
|
Loading…
x
Reference in New Issue
Block a user