mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-07 19:46:53 +00:00
Compare commits
28 Commits
dependabot
...
user/balal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79cc244351 | ||
|
|
e73839c7f2 | ||
|
|
b9dffdd990 | ||
|
|
13811b1d44 | ||
|
|
1643ce4dbb | ||
|
|
7abd65ac4b | ||
|
|
c731eb9cf9 | ||
|
|
c534b2d74b | ||
|
|
98fb7a5fd6 | ||
|
|
e34f68b162 | ||
|
|
7ab57c9ec4 | ||
|
|
7e1343e84f | ||
|
|
95b43d83d1 | ||
|
|
f9494030ac | ||
|
|
a1087b2626 | ||
|
|
5aeca52234 | ||
|
|
6c77430775 | ||
|
|
7bc78f126b | ||
|
|
e3ef515d8b | ||
|
|
7ffc1bc35a | ||
|
|
6add6d2e86 | ||
|
|
a089bac80e | ||
|
|
c0b5e185aa | ||
|
|
986fe39d07 | ||
|
|
fd511f2706 | ||
|
|
c35e23eb47 | ||
|
|
80be52685f | ||
|
|
d29fd6e957 |
44578
package-lock.json
generated
44578
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.10.5",
|
||||
"@azure/cosmos": "3.16.1",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.2.1",
|
||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
||||
@@ -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",
|
||||
|
||||
@@ -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==";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResourceType } from "@azure/cosmos/dist-esm/common/constants";
|
||||
import { ResourceType } from "@azure/cosmos";
|
||||
import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { endpoint, getTokenFromAuthService, requestPlugin, tokenProvider } from "./CosmosClient";
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
||||
import { CosmosHeaders } from "@azure/cosmos/dist-esm";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@@ -9,7 +7,7 @@ import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||
|
||||
const _global = typeof self === "undefined" ? window : self;
|
||||
|
||||
export const tokenProvider = async (requestInfo: RequestInfo) => {
|
||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||
|
||||
if (userContext.features.enableAadDataPlane && userContext.aadToken) {
|
||||
@@ -20,13 +18,13 @@ export const tokenProvider = async (requestInfo: RequestInfo) => {
|
||||
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
if (userContext.masterKey) {
|
||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
@@ -89,7 +87,7 @@ let _client: Cosmos.CosmosClient;
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) return _client;
|
||||
|
||||
let _defaultHeaders: CosmosHeaders = {};
|
||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { ContainerResponse, DatabaseResponse } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DatabaseResponse } from "@azure/cosmos";
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { DatabaseRequest, DatabaseResponse } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos";
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { client } from "../CosmosClient";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readOffers } from "./readOffers";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ContainerDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Collection } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { OfferDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
@@ -7,6 +7,11 @@ export interface DatabaseAccount {
|
||||
type: string;
|
||||
kind: string;
|
||||
properties: DatabaseAccountExtendedProperties;
|
||||
systemData?: DatabaseAccountSystemData;
|
||||
}
|
||||
|
||||
export interface DatabaseAccountSystemData {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface DatabaseAccountExtendedProperties {
|
||||
@@ -433,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;
|
||||
@@ -469,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;
|
||||
}
|
||||
|
||||
@@ -558,4 +570,5 @@ export enum PhoenixErrorType {
|
||||
RegionNotServicable = "RegionNotServicable",
|
||||
SubscriptionNotAllowed = "SubscriptionNotAllowed",
|
||||
UnknownError = "UnknownError",
|
||||
PhoenixFlightFallback = "PhoenixFlightFallback",
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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} />;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -42,6 +42,10 @@
|
||||
width: 18px;
|
||||
color: rgb(0, 120, 212);
|
||||
}
|
||||
.fonticoncustom {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.status {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
@@ -154,6 +154,7 @@ export class NotebookContainerClient {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
poolId: PoolIdType.DefaultPoolId,
|
||||
};
|
||||
return await this.phoenixClient.resetContainer(provisionData);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Immutable from "immutable";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import loadTransform from "../NotebookComponent/loadTransform";
|
||||
@@ -100,6 +101,7 @@ export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaA
|
||||
// Only in cases where CosmosMongoKernel runs into an error we get a single output
|
||||
if (outputs.size === 1) {
|
||||
traceFailure(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
|
||||
Logger.logError(`Failed to analyze schema: ${JSON.stringify(data)}`, "SchemaAnalyzer/traceClickAnalyzeComplete");
|
||||
} else {
|
||||
traceSuccess(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType } from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo, ContainerInfo, PhoenixErrorType } from "../../Contracts/DataModels";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -1193,6 +1193,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
useIndexingForSharedThroughput: this.state.enableIndexing,
|
||||
isQuickstart: !!this.props.isQuickstart,
|
||||
};
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData);
|
||||
|
||||
@@ -1254,6 +1255,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
collection.expandCollection();
|
||||
useDatabases.getState().updateDatabase(database);
|
||||
useTeachingBubble.getState().setIsSampleDBExpanded(true);
|
||||
TelemetryProcessor.traceOpen(Action.LaunchUITour);
|
||||
}
|
||||
}
|
||||
this.setState({ isExecuting: false });
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@ import { Coachmark, DirectionalHint, Image, Link, Stack, TeachingBubbleContent,
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import * as React from "react";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import AddDatabaseIcon from "../../../images/AddDatabase.svg";
|
||||
import NewQueryIcon from "../../../images/AddSqlQuery_16x16.svg";
|
||||
import NewStoredProcedureIcon from "../../../images/AddStoredProcedure.svg";
|
||||
@@ -48,8 +50,6 @@ export interface SplashScreenProps {
|
||||
}
|
||||
|
||||
export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
private static readonly seeMoreItemTitle: string = "See more Cosmos DB documentation";
|
||||
private static readonly seeMoreItemUrl: string = "https://aka.ms/cosmosdbdocument";
|
||||
private static readonly dataModelingUrl = "https://docs.microsoft.com/azure/cosmos-db/modeling-data";
|
||||
private static readonly throughputEstimatorUrl = "https://cosmos.azure.com/capacitycalculator";
|
||||
private static readonly failoverUrl = "https://docs.microsoft.com/azure/cosmos-db/high-availability";
|
||||
@@ -201,10 +201,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: "Launch quick start",
|
||||
description: "Launch a quick start tutorial to get started with sample data",
|
||||
showLinkIcon: userContext.apiType === "Mongo",
|
||||
onClick: () =>
|
||||
onClick: () => {
|
||||
userContext.apiType === "Mongo"
|
||||
? window.open("http://aka.ms/mongodbquickstart", "_blank")
|
||||
: this.container.onNewCollectionClicked({ isQuickstart: true }),
|
||||
: this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
||||
},
|
||||
};
|
||||
heroes.push(launchQuickstartBtn);
|
||||
} else if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
@@ -221,7 +223,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
iconSrc: ContainersIcon,
|
||||
title: `New ${getCollectionName()}`,
|
||||
description: "Create a new container for storage and throughput",
|
||||
onClick: () => this.container.onNewCollectionClicked(),
|
||||
onClick: () => {
|
||||
this.container.onNewCollectionClicked();
|
||||
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||
},
|
||||
};
|
||||
heroes.push(newContainerBtn);
|
||||
|
||||
@@ -397,29 +402,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
}
|
||||
}
|
||||
|
||||
private getCommonTasksItems(): JSX.Element {
|
||||
const commonTaskItems = this.createCommonTaskItems();
|
||||
return (
|
||||
<ul>
|
||||
{commonTaskItems.map((item) => (
|
||||
<li
|
||||
className="focusable"
|
||||
key={`${item.title}${item.description}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<img src={item.iconSrc} alt="" />
|
||||
<span className="oneLineContent" title={item.info}>
|
||||
{item.title}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
private top3Items(): JSX.Element {
|
||||
let items: { link: string; title: string; description: string }[];
|
||||
switch (userContext.apiType) {
|
||||
@@ -524,7 +506,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
{items.map((item, i) => (
|
||||
<Stack key={`top${i}`} style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
<Link href={item.link} target="_blank" style={{ marginRight: 5 }}>
|
||||
<Link
|
||||
onClick={() => traceOpen(Action.Top3ItemsClicked, { item: i + 1, apiType: userContext.apiType })}
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
style={{ marginRight: 5 }}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
@@ -665,7 +652,14 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
{items.map((item, i) => (
|
||||
<Stack key={`learningResource${i}`} style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
<Link href={item.link} target="_blank" style={{ marginRight: 5 }}>
|
||||
<Link
|
||||
onClick={() =>
|
||||
traceOpen(Action.LearningResourcesClicked, { item: i + 1, apiType: userContext.apiType })
|
||||
}
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
style={{ marginRight: 5 }}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
@@ -676,33 +670,4 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
private getTipItems(): JSX.Element {
|
||||
const tipsItems = this.createTipsItems();
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{tipsItems.map((item) => (
|
||||
<li
|
||||
className="tipContainer focusable"
|
||||
key={`${item.title}${item.description}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="link"
|
||||
>
|
||||
<div className="title" title={item.info}>
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="description">{item.description}</div>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<a role="link" href={SplashScreen.seeMoreItemUrl} rel="noreferrer" target="_blank" tabIndex={0}>
|
||||
{SplashScreen.seeMoreItemTitle}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { DefaultButton, IconButton, Image, Modal, PrimaryButton, Stack, Text } f
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import React, { useState } from "react";
|
||||
import Youtube from "react-youtube";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import Image1 from "../../../images/CarouselImage1.svg";
|
||||
import Image2 from "../../../images/CarouselImage2.svg";
|
||||
@@ -34,7 +36,6 @@ export const QuickstartCarousel: React.FC<QuickstartCarouselProps> = ({
|
||||
<DefaultButton text="Previous" style={{ margin: "16px 8px 16px 0" }} onClick={() => setPage(page - 1)} />
|
||||
)}
|
||||
<PrimaryButton
|
||||
id="carouselNextBtn"
|
||||
style={{ margin: "16px 16px 16px 0" }}
|
||||
text={page === 3 ? "Finish" : "Next"}
|
||||
onClick={() => {
|
||||
@@ -50,6 +51,10 @@ export const QuickstartCarousel: React.FC<QuickstartCarouselProps> = ({
|
||||
}
|
||||
setPage(page + 1);
|
||||
}
|
||||
|
||||
if (page === 3) {
|
||||
traceSuccess(Action.CompleteCarousel);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -74,7 +79,7 @@ const getHeaderText = (page: number): string => {
|
||||
const getContent = (page: number): JSX.Element => {
|
||||
switch (page) {
|
||||
case 1:
|
||||
return <Youtube videoId="Jvgh64rvdXU" />;
|
||||
return <Youtube videoId="Jvgh64rvdXU" onPlay={() => traceSuccess(Action.PlayCarouselVideo)} />;
|
||||
case 2:
|
||||
return <Image style={{ width: 640 }} src={Image1} />;
|
||||
case 3:
|
||||
|
||||
@@ -2,10 +2,17 @@ import { Link, Stack, TeachingBubble, Text } from "@fluentui/react";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import React from "react";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceCancel } from "Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
export const QuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
const { step, isSampleDBExpanded, isDocumentsTabOpened, sampleCollection, setStep } = useTeachingBubble();
|
||||
|
||||
const onDimissTeachingBubble = (): void => {
|
||||
setStep(0);
|
||||
traceCancel(Action.CancelUITour, { step });
|
||||
};
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
return isSampleDBExpanded ? (
|
||||
@@ -20,7 +27,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
setStep(2);
|
||||
},
|
||||
}}
|
||||
onDismiss={() => setStep(0)}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 1 of 7"
|
||||
>
|
||||
Start viewing and working with your data by opening Items under Data
|
||||
@@ -42,7 +49,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
text: "Previous",
|
||||
onClick: () => setStep(1),
|
||||
}}
|
||||
onDismiss={() => setStep(0)}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 2 of 7"
|
||||
>
|
||||
View item here using the items window. Additionally you can also filter items to be reviewed with the filter
|
||||
@@ -65,7 +72,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
text: "Previous",
|
||||
onClick: () => setStep(2),
|
||||
}}
|
||||
onDismiss={() => setStep(0)}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 3 of 7"
|
||||
>
|
||||
Add new item by copy / pasting JSON; or uploading a JSON
|
||||
@@ -85,7 +92,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
text: "Previous",
|
||||
onClick: () => setStep(3),
|
||||
}}
|
||||
onDismiss={() => setStep(0)}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 4 of 7"
|
||||
>
|
||||
Query your data using either the filter function or new query.
|
||||
@@ -105,7 +112,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
text: "Previous",
|
||||
onClick: () => setStep(4),
|
||||
}}
|
||||
onDismiss={() => setStep(0)}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 5 of 7"
|
||||
>
|
||||
Change throughput provisioned to your container according to your needs
|
||||
@@ -125,7 +132,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
text: "Previous",
|
||||
onClick: () => setStep(5),
|
||||
}}
|
||||
onDismiss={() => setStep(0)}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 6 of 7"
|
||||
>
|
||||
Visualize your data, store queries in an interactive document
|
||||
@@ -145,7 +152,7 @@ export const QuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
text: "Previous",
|
||||
onClick: () => setStep(6),
|
||||
}}
|
||||
onDismiss={() => setStep(0)}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 7 of 7"
|
||||
>
|
||||
<Stack>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
48
src/Localization/en/MaterializedViewsBuilder.json
Normal file
48
src/Localization/en/MaterializedViewsBuilder.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"MaterializedViewsBuilderDescription": "Provision a Materializedviews builder cluster for your Azure Cosmos DB account. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
|
||||
"MaterializedViewsBuilder": "Materializedviews Builder",
|
||||
"Provisioned": "Provisioned",
|
||||
"Deprovisioned": "Deprovisioned",
|
||||
"LearnAboutMaterializedViews": "Learn more about materializedviews.",
|
||||
"DeprovisioningDetailsText": "Learn more about materializedviews.",
|
||||
"MaterializedviewsBuilderPricing": "Learn more about materializedviews pricing.",
|
||||
"SKUs": "SKUs",
|
||||
"SKUsPlaceHolder": "Select SKUs",
|
||||
"NumberOfInstances": "Number of instances",
|
||||
"CosmosD2s": "Cosmos.D2s (General Purpose Cosmos Compute with 2 vCPUs, 8 GB Memory)",
|
||||
"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": "MaterializedViewsBuilder resource is being created.",
|
||||
"CreateInitializeTitle": "Provisioning resource",
|
||||
"CreateInitializeMessage": "Materializedviews Builder resource will be provisioned.",
|
||||
"CreateSuccessTitle": "Resource provisioned",
|
||||
"CreateSuccesseMessage": "Materializedviews Builder resource provisioned.",
|
||||
"CreateFailureTitle": "Failed to provision resource",
|
||||
"CreateFailureMessage": "Materializedviews Builder resource provisioning failed.",
|
||||
"UpdateMessage": "MaterializedViewsBuilder resource is being updated.",
|
||||
"UpdateInitializeTitle": "Updating resource",
|
||||
"UpdateInitializeMessage": "Materializedviews Builder resource will be updated.",
|
||||
"UpdateSuccessTitle": "Resource updated",
|
||||
"UpdateSuccesseMessage": "Materializedviews Builder resource updated.",
|
||||
"UpdateFailureTitle": "Failed to update resource",
|
||||
"UpdateFailureMessage": "Materializedviews Builder resource updation failed.",
|
||||
"DeleteMessage": "MaterializedViewsBuilder resource is being deleted.",
|
||||
"DeleteInitializeTitle": "Deleting resource",
|
||||
"DeleteInitializeMessage": "Materializedviews Builder resource will be deleted.",
|
||||
"DeleteSuccessTitle": "Resource deleted",
|
||||
"DeleteSuccesseMessage": "Materializedviews Builder resource deleted.",
|
||||
"DeleteFailureTitle": "Failed to delete resource",
|
||||
"DeleteFailureMessage": "Materializedviews Builder resource deletion failed.",
|
||||
"ApproximateCost": "Approximate Cost Per Hour",
|
||||
"CostText": "Hourly cost of the Materializedviews Builder resource depends on the SKU selection, number of instances per region, and number of regions.",
|
||||
"MetricsString": "Metrics",
|
||||
"MetricsText": "Monitor the CPU and memory usage for the Materializedviews Builder instances in ",
|
||||
"MetricsBlade": "the metrics blade.",
|
||||
"MonitorUsage": "Monitor Usage",
|
||||
"ResizingDecisionText": "To understand if the Materializedviews Builder is the right size, ",
|
||||
"ResizingDecisionLink": "learn more about Materializedviews Builder sizing.",
|
||||
"WarningBannerOnUpdate": "Adding or modifying Materializedviews Builder instances may affect your bill.",
|
||||
"WarningBannerOnDelete": "After deprovisioning the Materializedviews Builder, your materializedviews will not be updated with new source changes anymore. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition."
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
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 MaterializedViewsBuilder from "./MaterializedViewsBuilder";
|
||||
import {
|
||||
FetchPricesResponse,
|
||||
PriceMapAndCurrencyCode,
|
||||
RegionsResponse,
|
||||
MaterializedViewsBuilderServiceResource,
|
||||
UpdateMaterializedViewsBuilderRequestParameters,
|
||||
} from "./MaterializedViewsBuilderTypes";
|
||||
|
||||
const apiVersion = "2021-07-01-preview";
|
||||
|
||||
export enum ResourceStatus {
|
||||
Running = "Running",
|
||||
Creating = "Creating",
|
||||
Updating = "Updating",
|
||||
Deleting = "Deleting",
|
||||
}
|
||||
|
||||
export interface MaterializedViewsBuilderResponse {
|
||||
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/materializedviewsBuilder`;
|
||||
};
|
||||
|
||||
export const updateMaterializedViewsBuilderResource = async (sku: string, instances: number): Promise<string> => {
|
||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
||||
const body: UpdateMaterializedViewsBuilderRequestParameters = {
|
||||
properties: {
|
||||
instanceSize: sku,
|
||||
instanceCount: instances,
|
||||
serviceType: "materializedviewsBuilder",
|
||||
},
|
||||
};
|
||||
const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: MaterializedViewsBuilder.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: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, updateTimeStamp);
|
||||
throw e;
|
||||
}
|
||||
return armRequestResult?.operationStatusUrl;
|
||||
};
|
||||
|
||||
export const deleteMaterializedViewsBuilderResource = async (): Promise<string> => {
|
||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
||||
const telemetryData = { httpMethod: "DELETE", selfServeClassName: MaterializedViewsBuilder.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: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, deleteTimeStamp);
|
||||
throw e;
|
||||
}
|
||||
return armRequestResult?.operationStatusUrl;
|
||||
};
|
||||
|
||||
export const getMaterializedViewsBuilderResource = async (): Promise<MaterializedViewsBuilderServiceResource> => {
|
||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
||||
const telemetryData = { httpMethod: "GET", selfServeClassName: MaterializedViewsBuilder.name };
|
||||
const getResourceTimeStamp = selfServeTraceStart(telemetryData);
|
||||
let armRequestResult;
|
||||
try {
|
||||
armRequestResult = await armRequestWithoutPolling<MaterializedViewsBuilderServiceResource>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "GET",
|
||||
apiVersion,
|
||||
});
|
||||
selfServeTraceSuccess(telemetryData, getResourceTimeStamp);
|
||||
} catch (e) {
|
||||
const failureTelemetry = { e, selfServeClassName: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, getResourceTimeStamp);
|
||||
throw e;
|
||||
}
|
||||
return armRequestResult?.result;
|
||||
};
|
||||
|
||||
export const getCurrentProvisioningState = async (): Promise<MaterializedViewsBuilderResponse> => {
|
||||
try {
|
||||
const response = await getMaterializedViewsBuilderResource();
|
||||
return {
|
||||
sku: response.properties.instanceSize,
|
||||
instances: response.properties.instanceCount,
|
||||
status: response.properties.status,
|
||||
endpoint: response.properties.MaterializedViewsBuilderEndPoint,
|
||||
};
|
||||
} catch (e) {
|
||||
return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
export const refreshMaterializedViewsBuilderProvisioning = async (): Promise<RefreshResult> => {
|
||||
try {
|
||||
const response = await getMaterializedViewsBuilderResource();
|
||||
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 getRegions = async (): Promise<Array<string>> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getRegions",
|
||||
description: "",
|
||||
selfServeClassName: MaterializedViewsBuilder.name,
|
||||
};
|
||||
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),
|
||||
method: "GET",
|
||||
apiVersion: "2021-07-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;
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
|
||||
return new Array<string>();
|
||||
}
|
||||
};
|
||||
|
||||
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
||||
};
|
||||
|
||||
export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promise<PriceMapAndCurrencyCode> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getPriceMapAndCurrencyCode",
|
||||
description: "fetch prices API call",
|
||||
selfServeClassName: MaterializedViewsBuilder.name,
|
||||
};
|
||||
const getPriceMapAndCurrencyCodeTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
const priceMap = new Map<string, Map<string, number>>();
|
||||
let currencyCode;
|
||||
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 MaterializedViews Builder - General Purpose'",
|
||||
},
|
||||
});
|
||||
|
||||
for (const item of response.result.Items) {
|
||||
if (currencyCode === undefined) {
|
||||
currencyCode = item.currencyCode;
|
||||
} else if (item.currencyCode !== currencyCode) {
|
||||
throw Error("Currency Code Mismatch: Currency code not same for all regions / skus.");
|
||||
}
|
||||
regionPriceMap.set(item.skuName, item.retailPrice);
|
||||
}
|
||||
priceMap.set(region, regionPriceMap);
|
||||
}
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
|
||||
return { priceMap: priceMap, currencyCode: currencyCode };
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
||||
return { priceMap: undefined, currencyCode: undefined };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,416 @@
|
||||
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
|
||||
import {
|
||||
selfServeTrace,
|
||||
selfServeTraceFailure,
|
||||
selfServeTraceStart,
|
||||
selfServeTraceSuccess,
|
||||
} from "../SelfServeTelemetryProcessor";
|
||||
import {
|
||||
ChoiceItem,
|
||||
Description,
|
||||
DescriptionType,
|
||||
Info,
|
||||
InputType,
|
||||
NumberUiType,
|
||||
OnSaveResult,
|
||||
RefreshResult,
|
||||
SelfServeBaseClass,
|
||||
SmartUiInput,
|
||||
} from "../SelfServeTypes";
|
||||
import { BladeType, generateBladeLink } from "../SelfServeUtils";
|
||||
import {
|
||||
deleteMaterializedViewsBuilderResource,
|
||||
getCurrentProvisioningState,
|
||||
getPriceMapAndCurrencyCode,
|
||||
getRegions,
|
||||
refreshMaterializedViewsBuilderProvisioning,
|
||||
updateMaterializedViewsBuilderResource,
|
||||
} from "./MaterializedViewsBuilder.rp";
|
||||
|
||||
const costPerHourDefaultValue: Description = {
|
||||
textTKey: "CostText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
};
|
||||
|
||||
const metricsStringValue: Description = {
|
||||
textTKey: "MetricsText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: generateBladeLink(BladeType.Metrics),
|
||||
textTKey: "MetricsBlade",
|
||||
},
|
||||
};
|
||||
|
||||
const CosmosD2s = "Cosmos.D2s";
|
||||
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 MaterializedViewsBuilderOriginallyEnabled = baselineValues.get("enableMaterializedViewsBuilder")
|
||||
?.value as boolean;
|
||||
const baselineInstances = baselineValues.get("instances")?.value as number;
|
||||
if (!MaterializedViewsBuilderOriginallyEnabled || baselineInstances !== newValue) {
|
||||
currentValues.set("warningBanner", {
|
||||
value: {
|
||||
textTKey: "WarningBannerOnUpdate",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
} 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 onEnableMaterializedViewsBuilderChange = (
|
||||
newValue: InputType,
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||
): Map<string, SmartUiInput> => {
|
||||
currentValues.set("enableMaterializedViewsBuilder", { value: newValue });
|
||||
const MaterializedViewsBuilderOriginallyEnabled = baselineValues.get("enableMaterializedViewsBuilder")
|
||||
?.value as boolean;
|
||||
if (MaterializedViewsBuilderOriginallyEnabled === 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("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-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
} 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-materializedviews",
|
||||
textTKey: "DeprovisioningDetailsText",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
|
||||
}
|
||||
const sku = currentValues.get("sku");
|
||||
const instances = currentValues.get("instances");
|
||||
const hideAttributes = newValue === undefined || !(newValue as boolean);
|
||||
currentValues.set("sku", {
|
||||
value: sku.value,
|
||||
hidden: hideAttributes,
|
||||
disabled: MaterializedViewsBuilderOriginallyEnabled,
|
||||
});
|
||||
currentValues.set("instances", {
|
||||
value: instances.value,
|
||||
hidden: hideAttributes,
|
||||
disabled: MaterializedViewsBuilderOriginallyEnabled,
|
||||
});
|
||||
|
||||
currentValues.set("metricsString", {
|
||||
value: metricsStringValue,
|
||||
hidden: !newValue || !MaterializedViewsBuilderOriginallyEnabled,
|
||||
});
|
||||
|
||||
return currentValues;
|
||||
};
|
||||
|
||||
const skuDropDownItems: ChoiceItem[] = [
|
||||
{ labelTKey: "CosmosD2s", key: CosmosD2s },
|
||||
{ labelTKey: "CosmosD4s", key: CosmosD4s },
|
||||
{ labelTKey: "CosmosD8s", key: CosmosD8s },
|
||||
{ labelTKey: "CosmosD16s", key: CosmosD16s },
|
||||
];
|
||||
|
||||
const getSkus = async (): Promise<ChoiceItem[]> => {
|
||||
return skuDropDownItems;
|
||||
};
|
||||
|
||||
const getInstancesMin = async (): Promise<number> => {
|
||||
return 1;
|
||||
};
|
||||
|
||||
const getInstancesMax = async (): Promise<number> => {
|
||||
return 5;
|
||||
};
|
||||
|
||||
const NumberOfInstancesDropdownInfo: Info = {
|
||||
messageTKey: "ResizingDecisionText",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-size",
|
||||
textTKey: "ResizingDecisionLink",
|
||||
},
|
||||
};
|
||||
|
||||
const ApproximateCostDropDownInfo: Info = {
|
||||
messageTKey: "CostText",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
};
|
||||
|
||||
let priceMap: Map<string, Map<string, number>>;
|
||||
let currencyCode: string;
|
||||
let regions: Array<string>;
|
||||
|
||||
const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "calculateCost",
|
||||
description: "performs final calculation",
|
||||
selfServeClassName: MaterializedViewsBuilder.name,
|
||||
};
|
||||
const calculateCostTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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}`,
|
||||
type: DescriptionType.Text,
|
||||
};
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, regions, priceMap, selfServeClassName: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, calculateCostTimestamp);
|
||||
|
||||
return costPerHourDefaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
@IsDisplayable()
|
||||
@RefreshOptions({ retryIntervalInMs: 20000 })
|
||||
export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
public onRefresh = async (): Promise<RefreshResult> => {
|
||||
return await refreshMaterializedViewsBuilderProvisioning();
|
||||
};
|
||||
|
||||
public onSave = async (
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: Map<string, SmartUiInput>
|
||||
): Promise<OnSaveResult> => {
|
||||
selfServeTrace({ selfServeClassName: MaterializedViewsBuilder.name });
|
||||
|
||||
const MaterializedViewsBuilderCurrentlyEnabled = currentValues.get("enableMaterializedViewsBuilder")
|
||||
?.value as boolean;
|
||||
const MaterializedViewsBuilderOriginallyEnabled = baselineValues.get("enableMaterializedViewsBuilder")
|
||||
?.value as boolean;
|
||||
|
||||
currentValues.set("warningBanner", undefined);
|
||||
|
||||
if (MaterializedViewsBuilderOriginallyEnabled) {
|
||||
if (!MaterializedViewsBuilderCurrentlyEnabled) {
|
||||
const operationStatusUrl = await deleteMaterializedViewsBuilderResource();
|
||||
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 updateMaterializedViewsBuilderResource(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 updateMaterializedViewsBuilderResource(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 enableMaterializedViewsBuilder will be true if it has not yet been enabled and false if it has.
|
||||
const defaults = new Map<string, SmartUiInput>();
|
||||
defaults.set("enableMaterializedViewsBuilder", { value: false });
|
||||
defaults.set("sku", { value: CosmosD2s, hidden: true });
|
||||
defaults.set("instances", { value: await getInstancesMin(), hidden: true });
|
||||
defaults.set("costPerHour", undefined);
|
||||
defaults.set("metricsString", {
|
||||
value: undefined,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
regions = await getRegions();
|
||||
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(regions);
|
||||
priceMap = priceMapAndCurrencyCode.priceMap;
|
||||
currencyCode = priceMapAndCurrencyCode.currencyCode;
|
||||
|
||||
const response = await getCurrentProvisioningState();
|
||||
if (response.status && response.status !== "Deleting") {
|
||||
defaults.set("enableMaterializedViewsBuilder", { value: true });
|
||||
defaults.set("sku", { value: response.sku, disabled: true });
|
||||
defaults.set("instances", { value: response.instances, disabled: false });
|
||||
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
|
||||
|
||||
defaults.set("metricsString", {
|
||||
value: metricsStringValue,
|
||||
hidden: false,
|
||||
});
|
||||
}
|
||||
defaults.set("warningBanner", undefined);
|
||||
return defaults;
|
||||
};
|
||||
|
||||
@Values({
|
||||
isDynamicDescription: true,
|
||||
})
|
||||
warningBanner: string;
|
||||
|
||||
@Values({
|
||||
description: {
|
||||
textTKey: "MaterializedViewsBuilderDescription",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviews",
|
||||
textTKey: "LearnAboutMaterializedViews",
|
||||
},
|
||||
},
|
||||
})
|
||||
description: string;
|
||||
|
||||
@OnChange(onEnableMaterializedViewsBuilderChange)
|
||||
@Values({
|
||||
labelTKey: "MaterializedViewsBuilder",
|
||||
trueLabelTKey: "Provisioned",
|
||||
falseLabelTKey: "Deprovisioned",
|
||||
})
|
||||
enableMaterializedViewsBuilder: 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: "MonitorUsage",
|
||||
description: metricsStringValue,
|
||||
})
|
||||
metricsString: string;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
export type MaterializedViewsBuilderServiceResource = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
properties: MaterializedViewsBuilderServiceProps;
|
||||
locations: MaterializedViewsBuilderServiceLocations;
|
||||
};
|
||||
export type MaterializedViewsBuilderServiceProps = {
|
||||
serviceType: string;
|
||||
creationTime: string;
|
||||
status: string;
|
||||
instanceSize: string;
|
||||
instanceCount: number;
|
||||
MaterializedViewsBuilderEndPoint: string;
|
||||
};
|
||||
|
||||
export type MaterializedViewsBuilderServiceLocations = {
|
||||
location: string;
|
||||
status: string;
|
||||
MaterializedViewsBuilderEndpoint: string;
|
||||
};
|
||||
|
||||
export type UpdateMaterializedViewsBuilderRequestParameters = {
|
||||
properties: UpdateMaterializedViewsBuilderRequestProperties;
|
||||
};
|
||||
|
||||
export type UpdateMaterializedViewsBuilderRequestProperties = {
|
||||
instanceSize: string;
|
||||
instanceCount: number;
|
||||
serviceType: string;
|
||||
};
|
||||
|
||||
export type FetchPricesResponse = {
|
||||
Items: Array<PriceItem>;
|
||||
NextPageLink: string | undefined;
|
||||
Count: number;
|
||||
};
|
||||
|
||||
export type PriceMapAndCurrencyCode = {
|
||||
priceMap: Map<string, Map<string, number>>;
|
||||
currencyCode: string;
|
||||
};
|
||||
|
||||
export type PriceItem = {
|
||||
retailPrice: number;
|
||||
skuName: string;
|
||||
currencyCode: string;
|
||||
};
|
||||
|
||||
export type RegionsResponse = {
|
||||
locations: Array<RegionItem>;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type RegionItem = {
|
||||
locationName: string;
|
||||
};
|
||||
@@ -58,6 +58,14 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
await loadTranslations(graphAPICompute.constructor.name);
|
||||
return graphAPICompute.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.materializedviewsbuilder: {
|
||||
const MaterializedViewsBuilder = await import(
|
||||
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
||||
);
|
||||
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
||||
await loadTranslations(materializedViewsBuilder.constructor.name);
|
||||
return materializedViewsBuilder.toSelfServeDescriptor();
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export enum SelfServeType {
|
||||
example = "example",
|
||||
sqlx = "sqlx",
|
||||
graphapicompute = "graphapicompute",
|
||||
materializedviewsbuilder = "materializedviewsbuilder",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import SqlX from "./SqlX";
|
||||
import {
|
||||
FetchPricesResponse,
|
||||
PriceMapAndCurrencyCode,
|
||||
RegionItem,
|
||||
RegionsResponse,
|
||||
SqlxServiceResource,
|
||||
UpdateDedicatedGatewayRequestParameters,
|
||||
@@ -139,7 +140,7 @@ const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: str
|
||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
|
||||
};
|
||||
|
||||
export const getRegions = async (): Promise<Array<string>> => {
|
||||
export const getRegions = async (): Promise<Array<RegionItem>> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getRegions",
|
||||
@@ -149,8 +150,6 @@ export const getRegions = async (): Promise<Array<string>> => {
|
||||
const getRegionsTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
const regions = new Array<string>();
|
||||
|
||||
const response = await armRequestWithoutPolling<RegionsResponse>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
|
||||
@@ -158,20 +157,12 @@ export const getRegions = async (): Promise<Array<string>> => {
|
||||
apiVersion: "2021-04-01-preview",
|
||||
});
|
||||
|
||||
if (response.result.location !== undefined) {
|
||||
regions.push(response.result.location.split(" ").join("").toLowerCase());
|
||||
} else {
|
||||
for (const location of response.result.locations) {
|
||||
regions.push(location.locationName.split(" ").join("").toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getRegionsTimestamp);
|
||||
return regions;
|
||||
return response.result.properties.locations;
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
|
||||
return new Array<string>();
|
||||
return new Array<RegionItem>();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,7 +170,7 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
||||
};
|
||||
|
||||
export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promise<PriceMapAndCurrencyCode> => {
|
||||
export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Promise<PriceMapAndCurrencyCode> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getPriceMapAndCurrencyCode",
|
||||
@@ -191,7 +182,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
|
||||
try {
|
||||
const priceMap = new Map<string, Map<string, number>>();
|
||||
let currencyCode;
|
||||
for (const region of regions) {
|
||||
for (const regionItem of regions) {
|
||||
const regionPriceMap = new Map<string, number>();
|
||||
|
||||
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
||||
@@ -202,7 +193,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
|
||||
queryParams: {
|
||||
filter:
|
||||
"armRegionName eq '" +
|
||||
region +
|
||||
regionItem.locationName.split(" ").join("").toLowerCase() +
|
||||
"' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
|
||||
},
|
||||
});
|
||||
@@ -215,7 +206,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
|
||||
}
|
||||
regionPriceMap.set(item.skuName, item.retailPrice);
|
||||
}
|
||||
priceMap.set(region, regionPriceMap);
|
||||
priceMap.set(regionItem.locationName, regionPriceMap);
|
||||
}
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RegionItem } from "SelfServe/SqlX/SqlxTypes";
|
||||
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
|
||||
import {
|
||||
selfServeTrace,
|
||||
@@ -208,7 +209,7 @@ const ApproximateCostDropDownInfo: Info = {
|
||||
|
||||
let priceMap: Map<string, Map<string, number>>;
|
||||
let currencyCode: string;
|
||||
let regions: Array<string>;
|
||||
let regions: Array<RegionItem>;
|
||||
|
||||
const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
const telemetryData = {
|
||||
@@ -221,27 +222,47 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
|
||||
try {
|
||||
let costPerHour = 0;
|
||||
for (const region of regions) {
|
||||
const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", ""));
|
||||
let costBreakdown = "";
|
||||
for (const regionItem of regions) {
|
||||
const incrementalCost = priceMap.get(regionItem.locationName).get(skuName.replace("Cosmos.", ""));
|
||||
if (incrementalCost === undefined) {
|
||||
throw new Error("Value not found in map.");
|
||||
throw new Error(`${regionItem.locationName} not found in price map.`);
|
||||
} else if (incrementalCost === 0) {
|
||||
throw new Error(`${regionItem.locationName} cost per hour = 0`);
|
||||
}
|
||||
costPerHour += incrementalCost;
|
||||
|
||||
let regionalInstanceCount = instanceCount;
|
||||
if (regionItem.isZoneRedundant) {
|
||||
regionalInstanceCount = Math.ceil(instanceCount * 1.5);
|
||||
}
|
||||
|
||||
const regionalCostPerHour = incrementalCost * regionalInstanceCount;
|
||||
costBreakdown += `
|
||||
${regionItem.locationName} ${regionItem.isZoneRedundant ? "(AZ)" : ""}
|
||||
${regionalCostPerHour} ${currencyCode} (${regionalInstanceCount} instances * ${incrementalCost} ${currencyCode})\
|
||||
`;
|
||||
|
||||
if (regionalCostPerHour === 0) {
|
||||
throw new Error(`${regionItem.locationName} Cost per hour = 0`);
|
||||
}
|
||||
|
||||
costPerHour += regionalCostPerHour;
|
||||
}
|
||||
|
||||
if (costPerHour === 0) {
|
||||
throw new Error("Cost per hour = 0");
|
||||
}
|
||||
|
||||
costPerHour *= instanceCount;
|
||||
costPerHour = Math.round(costPerHour * 100) / 100;
|
||||
|
||||
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
|
||||
return {
|
||||
textTKey: `${costPerHour} ${currencyCode}`,
|
||||
textTKey: `${costPerHour} ${currencyCode}
|
||||
${costBreakdown}`,
|
||||
type: DescriptionType.Text,
|
||||
};
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
const failureTelemetry = { err, regions, priceMap, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, calculateCostTimestamp);
|
||||
|
||||
|
||||
@@ -48,10 +48,14 @@ export type PriceItem = {
|
||||
};
|
||||
|
||||
export type RegionsResponse = {
|
||||
properties: RegionsProperties;
|
||||
};
|
||||
|
||||
export type RegionsProperties = {
|
||||
locations: Array<RegionItem>;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type RegionItem = {
|
||||
locationName: string;
|
||||
isZoneRedundant: boolean;
|
||||
};
|
||||
|
||||
@@ -121,6 +121,15 @@ export enum Action {
|
||||
ExpandAddCollectionPaneAdvancedSection,
|
||||
SchemaAnalyzerClickAnalyze,
|
||||
SelfServeComponent,
|
||||
LaunchQuickstart,
|
||||
NewContainerHomepage,
|
||||
Top3ItemsClicked,
|
||||
LearningResourcesClicked,
|
||||
PlayCarouselVideo,
|
||||
OpenCarousel,
|
||||
CompleteCarousel,
|
||||
LaunchUITour,
|
||||
CancelUITour,
|
||||
}
|
||||
|
||||
export const ActionModifiers = {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { AuthType } from "./AuthType";
|
||||
import { DatabaseAccount } from "./Contracts/DataModels";
|
||||
import { SubscriptionType } from "./Contracts/SubscriptionType";
|
||||
@@ -56,6 +58,8 @@ interface UserContext {
|
||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra";
|
||||
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod" | "dev";
|
||||
|
||||
const ONE_WEEK_IN_MS = 604800000;
|
||||
|
||||
const features = extractFeatures();
|
||||
const { enableSDKoperations: useSDKOperations } = features;
|
||||
|
||||
@@ -71,12 +75,33 @@ const userContext: UserContext = {
|
||||
collectionCreationDefaults: CollectionCreationDefaults,
|
||||
};
|
||||
|
||||
function isAccountNewerThanThresholdInMs(createdAt: string, threshold: number) {
|
||||
let createdAtMs: number = Date.parse(createdAt);
|
||||
if (isNaN(createdAtMs)) {
|
||||
createdAtMs = 0;
|
||||
}
|
||||
|
||||
const nowMs: number = Date.now();
|
||||
const millisecsSinceAccountCreation = nowMs - createdAtMs;
|
||||
return threshold > millisecsSinceAccountCreation;
|
||||
}
|
||||
|
||||
function updateUserContext(newContext: Partial<UserContext>): void {
|
||||
if (newContext.databaseAccount) {
|
||||
newContext.apiType = apiType(newContext.databaseAccount);
|
||||
if (!localStorage.getItem(newContext.databaseAccount.id)) {
|
||||
|
||||
const isNewAccount = isAccountNewerThanThresholdInMs(
|
||||
newContext.databaseAccount?.systemData?.createdAt || "",
|
||||
ONE_WEEK_IN_MS
|
||||
);
|
||||
|
||||
if (
|
||||
!localStorage.getItem(newContext.databaseAccount.id) &&
|
||||
(userContext.isTryCosmosDBSubscription || isNewAccount)
|
||||
) {
|
||||
useCarousel.getState().setShouldOpen(true);
|
||||
localStorage.setItem(newContext.databaseAccount.id, "true");
|
||||
traceOpen(Action.OpenCarousel);
|
||||
}
|
||||
}
|
||||
Object.assign(userContext, newContext);
|
||||
|
||||
@@ -352,6 +352,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
hasWriteAccess: inputs.hasWriteAccess ?? true,
|
||||
addCollectionFlight: inputs.addCollectionDefaultFlight || CollectionCreation.DefaultAddCollectionDefaultFlight,
|
||||
collectionCreationDefaults: inputs.defaultCollectionThroughput,
|
||||
isTryCosmosDBSubscription: inputs.isTryCosmosDBSubscription,
|
||||
});
|
||||
if (inputs.features) {
|
||||
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
||||
|
||||
@@ -13,10 +13,6 @@ test("Cassandra keyspace and table CRUD", async () => {
|
||||
await page.waitForSelector("iframe");
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
// Click through quick start carousel
|
||||
await explorer.click("#carouselNextBtn");
|
||||
await explorer.click("#carouselNextBtn");
|
||||
|
||||
await explorer.click('[data-test="New Table"]');
|
||||
await explorer.click('[aria-label="Keyspace id"]');
|
||||
await explorer.fill('[aria-label="Keyspace id"]', keyspaceId);
|
||||
|
||||
@@ -12,10 +12,6 @@ test("Graph CRUD", async () => {
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner");
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
// Click through quick start carousel
|
||||
await explorer.click("#carouselNextBtn");
|
||||
await explorer.click("#carouselNextBtn");
|
||||
|
||||
// Create new database and graph
|
||||
await explorer.click('[data-test="New Graph"]');
|
||||
await explorer.fill('[aria-label="New database id"]', databaseId);
|
||||
|
||||
@@ -12,11 +12,6 @@ test("Mongo CRUD", async () => {
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
// Click through quick start carousel
|
||||
await explorer.click("#carouselNextBtn");
|
||||
await explorer.click("#carouselNextBtn");
|
||||
await explorer.click("#carouselNextBtn");
|
||||
|
||||
// Create new database and collection
|
||||
await explorer.click('[data-test="New Collection"]');
|
||||
await explorer.fill('[aria-label="New database id"]', databaseId);
|
||||
|
||||
@@ -12,11 +12,6 @@ test("Mongo CRUD", async () => {
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner");
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
// Click through quick start carousel
|
||||
await explorer.click("#carouselNextBtn");
|
||||
await explorer.click("#carouselNextBtn");
|
||||
await explorer.click("#carouselNextBtn");
|
||||
|
||||
// Create new database and collection
|
||||
await explorer.click('[data-test="New Collection"]');
|
||||
await explorer.fill('[aria-label="New database id"]', databaseId);
|
||||
|
||||
@@ -12,11 +12,6 @@ test("SQL CRUD", async () => {
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us");
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
// Click through quick start carousel
|
||||
await explorer.click("#carouselNextBtn");
|
||||
await explorer.click("#carouselNextBtn");
|
||||
await explorer.click("#carouselNextBtn");
|
||||
|
||||
await explorer.click('[data-test="New Container"]');
|
||||
await explorer.fill('[aria-label="New database id"]', databaseId);
|
||||
await explorer.fill('[aria-label="Container id"]', containerId);
|
||||
|
||||
@@ -12,10 +12,6 @@ test("Tables CRUD", async () => {
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner");
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
// Click through quick start carousel
|
||||
await explorer.click("#carouselNextBtn");
|
||||
await explorer.click("#carouselNextBtn");
|
||||
|
||||
await page.waitForSelector('text="Querying databases"', { state: "detached" });
|
||||
await explorer.click('[data-test="New Table"]');
|
||||
await explorer.fill('[aria-label="Table id"]', tableId);
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"types": ["jest"],
|
||||
"baseUrl": "src"
|
||||
"baseUrl": "src",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"typedocOptions": {
|
||||
"entryPoints": [
|
||||
|
||||
Reference in New Issue
Block a user