Compare commits

..

24 Commits

Author SHA1 Message Date
Bala Lakshmi Narayanasami
79cc244351 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into user/balalakshmin/chatbot 2022-06-23 15:04:54 +05:30
Bala Lakshmi Narayanasami
e73839c7f2 Ignore ts errors in lib 2022-06-23 14:57:44 +05:30
Mathieu Tremblay
b9dffdd990 Rename properties from notebookService to phoenixService in phoenix c… (#1263)
* Rename properties from notebookService to phoenixService in phoenix client


Co-authored-by: kcheekuri <kcheekuri@microsoft.com>
2022-06-21 14:19:45 -04:00
Karthik chakravarthy
13811b1d44 Update allocate call (#1290) 2022-06-21 08:48:18 -04:00
Tanuj Mittal
1643ce4dbb Log errors by Schema Analyzer (#1293) 2022-06-21 05:07:49 +05:30
Karthik chakravarthy
7abd65ac4b Enable phoenix based of allowed subscription and flights (#1291)
* Enable phoenix based of allowed subscription and flights
2022-06-15 17:08:06 -04:00
Tanuj Mittal
c731eb9cf9 Fix Official Samples not loading when Gallery tab is opened (#1282) 2022-06-09 02:30:49 +05:30
siddjoshi-ms
c534b2d74b Sqlx az portal cost estimate (#1264)
* added isZoneRedundant to rp call

* Pass region item everywhere

* cost breakdown string added
2022-06-08 10:16:43 -07:00
Bala Lakshmi Narayanasami
95b43d83d1 Compile errors fixed 2022-05-11 15:12:55 +05:30
Bala Lakshmi Narayanasami
f9494030ac Format fix 2022-05-10 18:08:25 +05:30
Bala Lakshmi Narayanasami
a1087b2626 Updating package-lock 2022-05-10 16:35:19 +05:30
Bala Lakshmi Narayanasami
5aeca52234 Reverting local change 2022-05-10 15:42:00 +05:30
Bala Lakshmi Narayanasami
6c77430775 Fixed lint errors 2022-05-10 15:34:58 +05:30
Bala Lakshmi Narayanasami
7bc78f126b Reverting unwanted changes 2022-05-09 19:08:26 +05:30
Bala Lakshmi Narayanasami
e3ef515d8b Reverting vscode settings changes 2022-05-09 19:05:42 +05:30
Bala Lakshmi Narayanasami
7ffc1bc35a Code cleanup 2022-05-09 19:04:17 +05:30
Bala Lakshmi Narayanasami
6add6d2e86 Changes for getting conversation token 2022-05-09 18:50:04 +05:30
Bala Lakshmi Narayanasami
a089bac80e Adding chatbot behind feature flag 2022-05-06 16:59:47 +05:30
Bala Lakshmi Narayanasami
c0b5e185aa Merge branch 'master' of https://github.com/Azure/cosmos-explorer into user/balalakshmin/chatbot 2022-05-04 16:55:03 +05:30
Bala Lakshmi Narayanasami
986fe39d07 Minor fix 2022-04-27 16:13:37 +05:30
Bala Lakshmi Narayanasami
fd511f2706 icon alignment fix 2022-02-17 21:02:42 +05:30
Bala Lakshmi Narayanasami
c35e23eb47 updating botframework version 2022-01-12 19:45:34 +05:30
Bala Lakshmi Narayanasami
80be52685f Merge branch 'master' of https://github.com/Azure/cosmos-explorer into user/balalakshmin/chatbot 2022-01-06 19:48:55 +05:30
Bala Lakshmi Narayanasami
d29fd6e957 Merge latest master changes to chatbot 2021-08-03 17:11:00 +05:30
24 changed files with 11244 additions and 40031 deletions

44530
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,6 +47,7 @@
"@types/node-fetch": "2.5.7",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
"botframework-webchat": "4.14.1",
"canvas": "file:./canvas",
"clean-webpack-plugin": "3.0.0",
"clipboard-copy": "4.0.1",

View File

@@ -354,6 +354,10 @@ export enum ContainerStatusType {
Disconnected = "Disconnected",
}
export enum PoolIdType {
DefaultPoolId = "default",
}
export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

View File

@@ -438,18 +438,25 @@ export interface NotebookWorkspaceConnectionInfo {
export interface ContainerInfo {
durationLeftInMinutes: number;
notebookServerInfo: NotebookWorkspaceConnectionInfo;
phoenixServerInfo: NotebookWorkspaceConnectionInfo;
status: ContainerStatusType;
}
export interface IProvisionData {
cosmosEndpoint: string;
poolId: string;
}
export interface IContainerData {
forwardingId: string;
}
export interface IDbAccountAllow {
status: number;
message?: string;
type?: string;
}
export interface IResponse<T> {
status: number;
data: T;
@@ -474,8 +481,8 @@ export interface IMaxUsersPerDbAccountExceeded extends IPhoenixError {
}
export interface IPhoenixConnectionInfoResult {
readonly notebookAuthToken?: string;
readonly notebookServerUrl?: string;
readonly authToken?: string;
readonly phoenixServiceUrl?: string;
readonly forwardingId?: string;
}
@@ -563,4 +570,5 @@ export enum PhoenixErrorType {
RegionNotServicable = "RegionNotServicable",
SubscriptionNotAllowed = "SubscriptionNotAllowed",
UnknownError = "UnknownError",
PhoenixFlightFallback = "PhoenixFlightFallback",
}

View File

@@ -1,13 +1,13 @@
/**
* React component for Command button component.
*/
import { Icon, IIconStyles } from "@fluentui/react";
import * as React from "react";
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
import { KeyCodes } from "../../../Common/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as StringUtils from "../../../Utils/StringUtils";
/**
* Options for this component
*/
@@ -243,6 +243,7 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
if (this.props.children && this.props.children.length > 0) {
contentClassName += " hasHiddenItems";
}
const iconButtonStyles: Partial<IIconStyles> = { root: { marginBottom: -3 } };
return (
<div className="commandButtonReact">
@@ -259,7 +260,18 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
onClick={(e: React.MouseEvent<HTMLSpanElement>) => this.commandClickCallback(e)}
>
<div className={contentClassName}>
<img className="commandIcon" src={this.props.iconSrc} alt={this.props.iconAlt} />
if (this.props.iconName){" "}
{
<div>
<Icon
styles={iconButtonStyles}
className="panelInfoIcon"
iconName={this.props.iconName}
ariaLabel="ChatBot"
/>
</div>
}{" "}
else {<img className="commandIcon" src={this.props.iconSrc} alt={this.props.iconAlt} />}
{CommandButtonComponent.renderLabel(this.props)}
</div>
</span>

View File

@@ -0,0 +1,41 @@
import { Activity } from "botframework-directlinejs";
import ReactWebChat, { createDirectLine } from "botframework-webchat";
import React from "react";
import * as _ from "underscore";
export interface SupportPaneComponentProps {
directLineToken: string;
userToken: string;
subId: string;
rg: string;
accName: string;
}
export class SupportPaneComponent extends React.Component<SupportPaneComponentProps> {
private readonly userId: string = _.uniqueId();
constructor(props: SupportPaneComponentProps) {
super(props);
}
public render(): JSX.Element {
const styleOptions = {
bubbleBackground: "rgba(0, 0, 255, .1)",
bubbleFromUserBackground: "rgba(0, 255, 0, .1)",
};
const directLine = createDirectLine({ token: this.props.directLineToken });
const dl = {
...directLine,
postActivity: (activity: Activity) => {
activity.channelData.token = this.props.userToken;
activity.channelData.subId = this.props.subId;
activity.channelData.rg = this.props.rg;
activity.channelData.accName = this.props.accName;
return directLine.postActivity(activity);
},
};
return <ReactWebChat directLine={dl} userID={this.userId} styleOptions={styleOptions} />;
}
}

View File

@@ -1,5 +1,6 @@
import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { configContext } from "ConfigContext";
import { IGalleryItem } from "Juno/JunoClient";
import * as ko from "knockout";
import React from "react";
@@ -9,7 +10,7 @@ import shallow from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
@@ -34,6 +35,7 @@ import { userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
@@ -85,6 +87,12 @@ export default class Explorer {
// Notebooks
public notebookManager?: NotebookManager;
public conversationToken: ko.Observable<string>;
public userToken: ko.Observable<string>;
public subId: ko.Observable<string>;
public rg: ko.Observable<string>;
public accName: ko.Observable<string>;
private _isInitializingNotebooks: boolean;
private notebookToImport: {
name: string;
@@ -106,6 +114,10 @@ export default class Explorer {
this.queriesClient = new QueriesClient(this);
this.conversationToken = ko.observable<string>();
this.generateConversationToken();
useSelectedNode.subscribe(() => {
// Make sure switching tabs restores tabs display
this.isTabsContentExpanded(false);
@@ -357,6 +369,7 @@ export default class Explorer {
) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
poolId: PoolIdType.DefaultPoolId,
};
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
@@ -369,8 +382,8 @@ export default class Explorer {
});
useNotebook.getState().setIsAllocating(true);
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
if (!connectionInfo?.data?.notebookServerUrl) {
throw new Error(`NotebookServerUrl is invalid!`);
if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`PhoenixServiceUrl is invalid!`);
}
await this.setNotebookInfo(connectionInfo, connectionStatus);
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
@@ -421,8 +434,8 @@ export default class Explorer {
notebookServerEndpoint:
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
userContext.features.notebookServerUrl) ||
connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
connectionInfo.data.phoenixServiceUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.authToken,
forwardingId: connectionInfo.data.forwardingId,
});
this.notebookManager?.notebookClient
@@ -454,6 +467,30 @@ export default class Explorer {
useDialog.getState().openDialog(resetConfirmationDialogProps);
}
private async generateConversationToken() {
const url = `${configContext.JUNO_ENDPOINT}/api/chatbot/bot${userContext.databaseAccount.id}/conversationToken`;
const authorizationHeader = getAuthorizationHeader();
const response = await fetch(url, {
method: "GET",
headers: {
[Constants.HttpHeaders.authorization]: authorizationHeader.token,
Accept: "application/json",
[Constants.HttpHeaders.contentType]: "application/json",
},
});
if (!response.ok) {
throw new Error(await response.json());
}
const tokenResponse: { conversationId: string; token: string; expires_in: number } = await response.json();
this.conversationToken(tokenResponse?.token);
if (tokenResponse?.expires_in) {
setTimeout(() => this.generateConversationToken(), (tokenResponse?.expires_in - 1000) * 1000);
}
}
private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise<boolean> {
if (!databaseAccount) {
return false;
@@ -497,8 +534,8 @@ export default class Explorer {
if (connectionInfo?.status !== HttpStatusCodes.OK) {
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.notebookServerUrl) {
throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`);
if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.setNotebookInfo(connectionInfo, connectionStatus);

View File

@@ -26,6 +26,7 @@ import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { SupportPaneComponent } from "../../Controls/SupportPaneComponent/SupportPaneComponent";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { OpenFullScreen } from "../../OpenFullScreen";
@@ -195,6 +196,35 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
const showOpenFullScreen =
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
if (userContext.authType === AuthType.AAD && userContext.features.enableChatbot) {
const label = "Chat Assistant";
const supportPaneButton: CommandButtonComponentProps = {
iconName: "ChatBot",
iconAlt: label,
onCommandClick: () => {
useSidePanel
.getState()
.openSidePanel(
"Chat Assistant (Beta)",
<SupportPaneComponent
directLineToken={container.conversationToken()}
userToken={userContext.authorizationToken}
subId={userContext.subscriptionId}
rg={userContext.resourceGroup}
accName={userContext.databaseAccount.name}
/>
);
},
commandButtonLabel: null,
ariaLabel: label,
tooltipText: label,
hasPopup: true,
disabled: false,
className: "fonticoncustom",
};
buttons.push(supportPaneButton);
}
if (showOpenFullScreen) {
const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = {

View File

@@ -42,6 +42,10 @@
width: 18px;
color: rgb(0, 120, 212);
}
.fonticoncustom {
padding-top: 12px;
}
.status {
position: relative;
display: block;

View File

@@ -5,7 +5,7 @@ import { useDialog } from "Explorer/Controls/Dialog";
import promiseRetry, { AbortError } from "p-retry";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../../Common/Constants";
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
@@ -154,6 +154,7 @@ export class NotebookContainerClient {
if (useNotebook.getState().isPhoenixNotebooks) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
poolId: PoolIdType.DefaultPoolId,
};
return await this.phoenixClient.resetContainer(provisionData);
}

View File

@@ -5,6 +5,7 @@ import Immutable from "immutable";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import * as Logger from "../../../Common/Logger";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import loadTransform from "../NotebookComponent/loadTransform";
@@ -100,6 +101,7 @@ export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaA
// Only in cases where CosmosMongoKernel runs into an error we get a single output
if (outputs.size === 1) {
traceFailure(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
Logger.logError(`Failed to analyze schema: ${JSON.stringify(data)}`, "SchemaAnalyzer/traceClickAnalyzeComplete");
} else {
traceSuccess(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
}

View File

@@ -4,12 +4,12 @@ import { PhoenixClient } from "Phoenix/PhoenixClient";
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType } from "../../Common/Constants";
import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
import { ContainerConnectionInfo, ContainerInfo, PhoenixErrorType } from "../../Contracts/DataModels";
import { useTabs } from "../../hooks/useTabs";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
@@ -96,7 +96,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
phoenixServerInfo: undefined,
},
isPhoenixNotebooks: undefined,
isPhoenixFeatures: undefined,
@@ -296,22 +296,30 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
useNotebook.getState().setContainerStatus({
status: undefined,
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
phoenixServerInfo: undefined,
});
},
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
getPhoenixStatus: async () => {
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
let isPhoenix = false;
if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) {
const phoenixClient = new PhoenixClient();
isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted());
let isPhoenixNotebooks = false;
let isPhoenixFeatures = false;
const isPublicInternetAllowed = isPublicInternetAccessAllowed();
const phoenixClient = new PhoenixClient();
const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus();
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks;
isPhoenixFeatures = isPublicInternetAllowed && userContext.features.phoenixFeatures;
} else {
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
}
} else {
isPhoenixNotebooks = isPhoenixFeatures = false;
}
const isPhoenixNotebooks = userContext.features.phoenixNotebooks && isPhoenix;
const isPhoenixFeatures = userContext.features.phoenixFeatures && isPhoenix;
set({ isPhoenixNotebooks: isPhoenixNotebooks });
set({ isPhoenixFeatures: isPhoenixFeatures });
}

View File

@@ -2,6 +2,7 @@ import { initializeIcons, Link, Text } from "@fluentui/react";
import "bootstrap/dist/css/bootstrap.css";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { userContext } from "UserContext";
import { initializeConfiguration } from "../ConfigContext";
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
import {
@@ -25,7 +26,9 @@ const onInit = async () => {
const props: GalleryAndNotebookViewerComponentProps = {
junoClient: new JunoClient(),
selectedTab: galleryViewerProps.selectedTab || GalleryTab.PublicGallery,
selectedTab:
galleryViewerProps.selectedTab ||
(userContext.features.publicGallery ? GalleryTab.PublicGallery : GalleryTab.OfficialSamples),
sortBy: galleryViewerProps.sortBy || SortBy.MostRecent,
searchText: galleryViewerProps.searchText,
};

View File

@@ -17,6 +17,7 @@ import {
ContainerConnectionInfo,
ContainerInfo,
IContainerData,
IDbAccountAllow,
IMaxAllocationTimeExceeded,
IMaxDbAccountsPerUserExceeded,
IMaxUsersPerDbAccountExceeded,
@@ -103,7 +104,7 @@ export class PhoenixClient {
const containerStatus = await response.json();
return {
durationLeftInMinutes: containerStatus?.durationLeftInMinutes,
notebookServerInfo: containerStatus?.notebookServerInfo,
phoenixServerInfo: containerStatus?.phoenixServerInfo,
status: ContainerStatusType.Active,
};
} else if (response.status === HttpStatusCodes.NotFound) {
@@ -147,7 +148,7 @@ export class PhoenixClient {
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
return {
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
phoenixServerInfo: undefined,
status: ContainerStatusType.Disconnected,
};
}
@@ -161,15 +162,17 @@ export class PhoenixClient {
}
}
public async isDbAcountWhitelisted(): Promise<boolean> {
public async getDbAccountAllowedStatus(): Promise<IDbAccountAllow> {
const startKey = TelemetryProcessor.traceStart(Action.PhoenixDBAccountAllowed, {
dataExplorerArea: Areas.Notebook,
});
let responseJson;
try {
const response = await window.fetch(`${this.getPhoenixControlPlanePathPrefix()}`, {
method: "GET",
headers: PhoenixClient.getHeaders(),
});
responseJson = await response?.json();
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received status code: ${response?.status}`);
}
@@ -180,7 +183,11 @@ export class PhoenixClient {
},
startKey
);
return response.status === HttpStatusCodes.OK;
return {
status: response.status,
message: responseJson?.message,
type: responseJson?.type,
};
} catch (error) {
TelemetryProcessor.traceFailure(
Action.PhoenixDBAccountAllowed,
@@ -192,7 +199,11 @@ export class PhoenixClient {
startKey
);
Logger.logError(getErrorMessage(error), "PhoenixClient/IsDbAcountWhitelisted");
return false;
return {
status: HttpStatusCodes.Forbidden,
message: responseJson?.message,
type: responseJson?.type,
};
}
}

View File

@@ -29,6 +29,7 @@ export type Features = {
readonly mongoProxyEndpoint?: string;
readonly mongoProxyAPIs?: string;
readonly enableThroughputCap: boolean;
readonly enableChatbot?: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
@@ -90,6 +91,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
notebooksDownBanner: "true" === get("notebooksDownBanner"),
enableThroughputCap: "true" === get("enablethroughputcap"),
enableChatbot: "true" === get("enablechatbot"),
};
}

View File

@@ -7,6 +7,7 @@ import SqlX from "./SqlX";
import {
FetchPricesResponse,
PriceMapAndCurrencyCode,
RegionItem,
RegionsResponse,
SqlxServiceResource,
UpdateDedicatedGatewayRequestParameters,
@@ -139,7 +140,7 @@ const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: str
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
};
export const getRegions = async (): Promise<Array<string>> => {
export const getRegions = async (): Promise<Array<RegionItem>> => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "getRegions",
@@ -149,8 +150,6 @@ export const getRegions = async (): Promise<Array<string>> => {
const getRegionsTimestamp = selfServeTraceStart(telemetryData);
try {
const regions = new Array<string>();
const response = await armRequestWithoutPolling<RegionsResponse>({
host: configContext.ARM_ENDPOINT,
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
@@ -158,20 +157,12 @@ export const getRegions = async (): Promise<Array<string>> => {
apiVersion: "2021-04-01-preview",
});
if (response.result.location !== undefined) {
regions.push(response.result.location.split(" ").join("").toLowerCase());
} else {
for (const location of response.result.locations) {
regions.push(location.locationName.split(" ").join("").toLowerCase());
}
}
selfServeTraceSuccess(telemetryData, getRegionsTimestamp);
return regions;
return response.result.properties.locations;
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
return new Array<string>();
return new Array<RegionItem>();
}
};
@@ -179,7 +170,7 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => {
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
};
export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promise<PriceMapAndCurrencyCode> => {
export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Promise<PriceMapAndCurrencyCode> => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "getPriceMapAndCurrencyCode",
@@ -191,7 +182,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
try {
const priceMap = new Map<string, Map<string, number>>();
let currencyCode;
for (const region of regions) {
for (const regionItem of regions) {
const regionPriceMap = new Map<string, number>();
const response = await armRequestWithoutPolling<FetchPricesResponse>({
@@ -202,7 +193,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
queryParams: {
filter:
"armRegionNameeq '" +
region +
regionItem.locationName.split(" ").join("").toLowerCase() +
"'andserviceFamilyeq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
},
});
@@ -215,7 +206,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
}
regionPriceMap.set(item.skuName, item.retailPrice);
}
priceMap.set(region, regionPriceMap);
priceMap.set(regionItem.locationName, regionPriceMap);
}
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);

View File

@@ -1,3 +1,4 @@
import { RegionItem } from "SelfServe/SqlX/SqlxTypes";
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
import {
selfServeTrace,
@@ -208,7 +209,7 @@ const ApproximateCostDropDownInfo: Info = {
let priceMap: Map<string, Map<string, number>>;
let currencyCode: string;
let regions: Array<string>;
let regions: Array<RegionItem>;
const calculateCost = (skuName: string, instanceCount: number): Description => {
const telemetryData = {
@@ -221,27 +222,47 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
try {
let costPerHour = 0;
for (const region of regions) {
const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", ""));
let costBreakdown = "";
for (const regionItem of regions) {
const incrementalCost = priceMap.get(regionItem.locationName).get(skuName.replace("Cosmos.", ""));
if (incrementalCost === undefined) {
throw new Error("Value not found in map.");
throw new Error(`${regionItem.locationName} not found in price map.`);
} else if (incrementalCost === 0) {
throw new Error(`${regionItem.locationName} cost per hour = 0`);
}
costPerHour += incrementalCost;
let regionalInstanceCount = instanceCount;
if (regionItem.isZoneRedundant) {
regionalInstanceCount = Math.ceil(instanceCount * 1.5);
}
const regionalCostPerHour = incrementalCost * regionalInstanceCount;
costBreakdown += `
${regionItem.locationName} ${regionItem.isZoneRedundant ? "(AZ)" : ""}
${regionalCostPerHour} ${currencyCode} (${regionalInstanceCount} instances * ${incrementalCost} ${currencyCode})\
`;
if (regionalCostPerHour === 0) {
throw new Error(`${regionItem.locationName} Cost per hour = 0`);
}
costPerHour += regionalCostPerHour;
}
if (costPerHour === 0) {
throw new Error("Cost per hour = 0");
}
costPerHour *= instanceCount;
costPerHour = Math.round(costPerHour * 100) / 100;
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
return {
textTKey: `${costPerHour} ${currencyCode}`,
textTKey: `${costPerHour} ${currencyCode}
${costBreakdown}`,
type: DescriptionType.Text,
};
} catch (err) {
alert(err);
const failureTelemetry = { err, regions, priceMap, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, calculateCostTimestamp);

View File

@@ -48,10 +48,14 @@ export type PriceItem = {
};
export type RegionsResponse = {
properties: RegionsProperties;
};
export type RegionsProperties = {
locations: Array<RegionItem>;
location: string;
};
export type RegionItem = {
locationName: string;
isZoneRedundant: boolean;
};

View File

@@ -21,7 +21,8 @@
"resolveJsonModule": true,
"noEmit": true,
"types": ["jest"],
"baseUrl": "src"
"baseUrl": "src",
"skipLibCheck": true
},
"typedocOptions": {
"entryPoints": [

View File

@@ -51,12 +51,9 @@
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"supports-color": {
"version": "7.1.0",
@@ -65,25 +62,6 @@
"requires": {
"has-flag": "^4.0.0"
}
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
}
}
}

View File

@@ -12,6 +12,6 @@
"dependencies": {
"chalk": "^4.1.0",
"moment": "^2.27.0",
"node-fetch": "^2.6.7"
"node-fetch": "^2.6.1"
}
}