Compare commits

..

31 Commits

Author SHA1 Message Date
kcheekuri
db8a6f9c5c Update comments-2 2021-12-11 17:42:26 -05:00
kcheekuri
7b332388e1 Format issue 2021-12-11 16:17:00 -05:00
kcheekuri
a9e1c2fba8 Merge branch 'master' into users/kcheekuri/updatePhoenixEndpoints 2021-12-11 16:04:46 -05:00
kcheekuri
004c38c6b8 Address review coments 2021-12-11 14:11:32 -05:00
kcheekuri
a4be933f70 Disable phoenix for vnet and firewall 2021-12-10 10:25:48 -05:00
victor-meng
ada95eae1f Fix execute sproc pane textfield focus issue (#1170)
* Fix execute sproc pane textfield focus issue

* Update snapshot
2021-12-08 15:41:27 -08:00
vaidankarswapnil
8a8c023d7b Fix Keyboard focus New Database button (#1167)
* Fix a11y new database button focus issue

* Update test snapshot and other issues

* fix issue for the menu button

* Issue fixed in Splash screen
2021-12-02 20:13:45 -08:00
Hardikkumar Nai
667b1e1486 1413651_Refresh_button_missing (#1169) 2021-12-02 20:12:57 -08:00
Sunil Kumar Yadav
203c2ac246 fixed horizontal scroll issue on zoom 400% (#1165)
Co-authored-by: sunilyadav <v-yadavsunil@microsoft.com>
2021-12-01 19:46:48 -08:00
victor-meng
5d235038ad Properly update table headers (#1166) 2021-11-30 15:36:35 -08:00
Srinath Narayanan
6b4d6f986e added github test env client id (#1168) 2021-12-01 03:38:38 +05:30
Karthik chakravarthy
e575b94ffa Add phoenix telemetry (#1164)
* Add phoenix telemetry

* Revert changes

* Update trace logs
2021-11-29 11:22:57 -05:00
vaidankarswapnil
42bdcaf8d1 Fix radio buttons present under 'Settings' blade like ‘Custom and Unlimited’ along with its label ‘Page options’ are not enclosed in fieldset/legend tag (#1100)
* Fix a11y setting pane radiobuttons issue

* Update test snapshot issue
2021-11-24 20:00:06 -08:00
victor-meng
94a03e5b03 Add Timestamp type to cassandra column types and wrap Timestamp value inside single quotes when creating queries (#1163) 2021-11-19 09:55:10 -08:00
kcheekuri
288c85d13c Update Api end points and add brs for allowlist 2021-11-16 07:27:00 -05:00
victor-meng
1155557af1 Check for -1 throughput cap value (#1159) 2021-11-10 21:43:04 -08:00
tarazou9
27a49e9aa9 add juno test3 to allow list (#1158)
* add juno test3 to allow list

* remove extra line
2021-11-10 17:05:31 +05:30
Srinath Narayanan
fa8be2bc0f fixed quickstarts (#1157) 2021-11-10 17:05:17 +05:30
Karthik chakravarthy
3aa4bbe266 Users/kcheekuri/phoenix heart beat retry with delay (#1153)
* Health check retry addition

* format issue

* Address comments

* Test Check

* Added await

* code cleanup
2021-11-09 18:08:17 +05:30
siddjoshi-ms
2dfabf3c69 Sqlx currency code fix (#1149)
* using currency code from fetch prices api

* formatting & linting fixes

* Update SqlX.rp.ts
2021-11-09 00:04:22 +05:30
victor-meng
a3d88af175 Fix throughputcap check (#1156) 2021-11-05 10:23:21 -07:00
Srinath Narayanan
5597a1e8b6 Changes to reset container workflow (#1155)
* reset changes

* undid config context changes

* renamed method
2021-11-04 21:55:41 +05:30
victor-meng
e3d5ad2ce8 Fix ARM api version (#1154) 2021-11-02 12:23:48 -07:00
victor-meng
64f36e2d28 Add throughput cap error message (#1151) 2021-10-30 19:45:16 -07:00
Srinath Narayanan
4ce1252e58 master/main fix (#1150) 2021-10-28 17:08:34 +05:30
Karthik chakravarthy
7d9faec81e Phoenix runtime - Reset workspace (#1136)
* Phoenix runtime - Reset workspace

* Format and Lint issues

* Typo issue

* Reset warning text change and create new context on allcation of new container

* Closing only notebook related

* resolved comments from previous PR

* On Schema Analyser allocate call

Co-authored-by: Srinath Narayanan <srnara@microsoft.com>
2021-10-22 10:41:13 -04:00
Karthik chakravarthy
22da3b90ef Phoenix Reconnect Integration (#1123)
* Reconnect integration

* git connection issue

* format issue

* Typo issue

* added constants

* Removed math.round for remainingTime

* code refctor for container status check

* disconnect text change
2021-10-22 14:34:38 +05:30
Srinath Narayanan
361ac45e52 Added notebooksDownBanner flight (#1146)
* set isNotebookEnabled to true

* lint and format fixes

* modified shell enabled

* added notebooks down banner flight

* fixed typo
2021-10-22 13:27:52 +05:30
Srinath Narayanan
8aa764079a Setting isNotebooKEnabled to true by default (#1145)
* set isNotebookEnabled to true

* lint and format fixes

* modified shell enabled
2021-10-22 11:48:40 +05:30
victor-meng
55837db65b Revert "Fix keyboard focus does not retain on 'New Database' button a… (#1139)
* Revert "Fix keyboard focus does not retain on 'New Database' button after closing the 'New Database' blade via ESC key (#1109)"

This reverts commit f7e7240010.

* Revert "Fix ally database panel open issue (#1120)"

This reverts commit ed1ffb692f.
2021-10-15 17:36:48 -07:00
victor-meng
9f27cb95b9 Only use the SET keyword once in the update query (#1138) 2021-10-15 12:33:59 -07:00
78 changed files with 6197 additions and 5510 deletions

View File

@@ -85,10 +85,14 @@ src/Explorer/Tables/DataTable/DataTableViewModel.ts
src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
src/Explorer/Tables/TableDataClient.ts
src/Explorer/Tables/TableEntityProcessor.ts
src/Explorer/Tables/Utilities.ts
src/Explorer/Tabs/ConflictsTab.ts
src/Explorer/Tabs/DatabaseSettingsTab.ts
src/Explorer/Tabs/DocumentsTab.test.ts
src/Explorer/Tabs/DocumentsTab.ts
src/Explorer/Tabs/GraphTab.ts
src/Explorer/Tabs/MongoDocumentsTab.ts
src/Explorer/Tabs/NotebookV2Tab.ts
src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/TabComponents.ts

View File

@@ -1,3 +1,4 @@
{
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
"IS_MPAC" : true
}

View File

@@ -1,3 +1,4 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com"
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"IS_MPAC" : false
}

View File

@@ -2077,7 +2077,7 @@ a:link {
.resourceTreeAndTabs {
display: flex;
flex: 1 1 auto;
overflow-x: auto;
overflow-x: clip;
overflow-y: auto;
height: 100%;
}
@@ -2245,7 +2245,7 @@ a:link {
}
.refreshColHeader {
padding: 3px 6px 6px 6px;
padding: 3px 6px 10px 0px !important;
}
.refreshColHeader:hover {
@@ -2869,31 +2869,31 @@ a:link {
}
}
settings-pane {
.settingsSection {
border-bottom: 1px solid @BaseMedium;
margin-right: 24px;
padding: @MediumSpace 0px;
.settingsSection {
border-bottom: 1px solid @BaseMedium;
margin-right: 24px;
padding: @MediumSpace 0px;
&:first-child {
padding-top: 0px;
}
&:first-child {
padding-top: 0px;
padding-bottom: 10px;
}
&:last-child {
border-bottom: none;
}
&:last-child {
border-bottom: none;
}
.settingsSectionPart {
padding-left: 8px;
}
.settingsSectionPart {
padding-left: 8px;
}
.settingsSectionLabel {
margin-bottom: @DefaultSpace;
}
.settingsSectionLabel {
margin-bottom: @DefaultSpace;
margin-right: 5px;
}
.pageOptionsPart {
padding-bottom: @MediumSpace;
}
.pageOptionsPart {
padding-bottom: @MediumSpace;
}
}

View File

@@ -97,6 +97,7 @@ export class Flights {
public static readonly PartitionKeyTest = "partitionkeytest";
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
public static readonly Phoenix = "phoenix";
public static readonly NotebooksDownBanner = "notebooksdownbanner";
}
export class AfecFeatures {
@@ -343,7 +344,12 @@ export enum ConnectionStatusType {
Connecting = "Connecting",
Connected = "Connected",
Failed = "Connection Failed",
ReConnect = "Reconnect",
Reconnect = "Reconnect",
}
export enum ContainerStatusType {
Active = "Active",
Disconnected = "Disconnected",
}
export const EmulatorMasterKey =
@@ -356,20 +362,25 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta
export class Notebook {
public static readonly defaultBasePath = "./notebooks";
public static readonly heartbeatDelayMs = 60000;
public static readonly containerStatusHeartbeatDelayMs = 30000;
public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000;
public static readonly memoryGuageToGB = 1048576;
public static readonly lowMemoryThreshold = 0.8;
public static readonly remainingTimeForAlert = 10;
public static readonly retryAttempts = 3;
public static readonly retryAttemptDelayMs = 5000;
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
public static readonly mongoShellTemporarilyDownMsg =
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
public static readonly cassandraShellTemporarilyDownMsg =
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
public static saveNotebookModalTitle = "Save Notebook in temporary workspace";
public static saveNotebookModalTitle = "Save notebook in temporary workspace";
public static saveNotebookModalContent =
"This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends.";
public static newNotebookModalTitle = "Create Notebook in temporary workspace";
public static newNotebookUploadModalTitle = "Upload Notebook in temporary workspace";
public static newNotebookModalTitle = "Create notebook in temporary workspace";
public static newNotebookUploadModalTitle = "Upload notebook to temporary workspace";
public static newNotebookModalContent1 =
"A temporary workspace will be created to enable you to work with notebooks. When the session expires, any notebooks in the workspace will be removed.";
public static newNotebookModalContent2 =
@@ -401,3 +412,11 @@ export class TerminalQueryParams {
public static readonly SubscriptionId = "subscriptionId";
public static readonly TerminalEndpoint = "terminalEndpoint";
}
export class JunoEndpoints {
public static readonly Test = "https://juno-test.documents-dev.windows-int.net";
public static readonly Test2 = "https://juno-test2.documents-dev.windows-int.net";
public static readonly Test3 = "https://juno-test3.documents-dev.windows-int.net";
public static readonly Prod = "https://tools.cosmos.azure.com";
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
}

View File

@@ -1,3 +1,5 @@
import { JunoEndpoints } from "Common/Constants";
export enum Platform {
Portal = "Portal",
Hosted = "Hosted",
@@ -23,7 +25,9 @@ export interface ConfigContext {
PROXY_PATH?: string;
JUNO_ENDPOINT: string;
GITHUB_CLIENT_ID: string;
GITHUB_TEST_ENV_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
IS_MPAC: boolean;
hostedExplorerURL: string;
armAPIVersion?: string;
allowedJunoOrigins: string[];
@@ -52,14 +56,17 @@ let configContext: Readonly<ConfigContext> = {
GRAPH_API_VERSION: "1.6",
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
IS_MPAC: false,
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
allowedJunoOrigins: [
"https://juno-test.documents-dev.windows-int.net",
"https://juno-test2.documents-dev.windows-int.net",
"https://tools.cosmos.azure.com",
"https://tools-staging.cosmos.azure.com",
JunoEndpoints.Test,
JunoEndpoints.Test2,
JunoEndpoints.Test3,
JunoEndpoints.Prod,
JunoEndpoints.Stage,
"https://localhost",
],
};

View File

@@ -1,4 +1,4 @@
import { ConnectionStatusType } from "../Common/Constants";
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
export interface DatabaseAccount {
id: string;
@@ -26,6 +26,8 @@ export interface DatabaseAccountExtendedProperties {
isVirtualNetworkFilterEnabled?: boolean;
ipRules?: IpRule[];
privateEndpointConnections?: unknown[];
capacity?: { totalThroughputLimit: number };
locations?: DatabaseAccountResponseLocation[];
}
export interface DatabaseAccountResponseLocation {
@@ -426,6 +428,32 @@ export interface OperationStatus {
export interface NotebookWorkspaceConnectionInfo {
authToken: string;
notebookServerEndpoint: string;
forwardingId: string;
}
export interface ContainerInfo {
durationLeftInMinutes: number;
notebookServerInfo: NotebookWorkspaceConnectionInfo;
status: ContainerStatusType;
}
export interface IProvisionData {
cosmosEndpoint: string;
}
export interface IContainerData {
forwardingId: string;
}
export interface IResponse<T> {
status: number;
data: T;
}
export interface IPhoenixConnectionInfoResult {
readonly notebookAuthToken?: string;
readonly notebookServerUrl?: string;
readonly forwardingId?: string;
}
export interface NotebookWorkspaceFeedResponse {

View File

@@ -83,7 +83,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: HostedTerminalIcon,
isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown,
isDisabled: useNotebook.getState().isShellEnabled && !useNotebook.getState().isPhoenix,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {

View File

@@ -13,7 +13,6 @@ import {
Link,
PrimaryButton,
ProgressIndicator,
Text,
TextField,
} from "@fluentui/react";
import React, { FC } from "react";
@@ -197,7 +196,7 @@ export const Dialog: FC = () => {
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link>
)}
{contentHtml && <Text>{contentHtml}</Text>}
{contentHtml}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter>
<PrimaryButton {...primaryButtonProps} />

View File

@@ -1,14 +1,14 @@
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "@fluentui/react";
import * as React from "react";
import * as Constants from "../../../Common/Constants";
import * as UrlUtility from "../../../Common/UrlUtility";
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import Explorer from "../../Explorer";
import { RepoListItem } from "./GitHubReposComponent";
import { ChildrenMargin } from "./GitHubStyleConstants";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as UrlUtility from "../../../Common/UrlUtility";
import Explorer from "../../Explorer";
export interface AddRepoComponentProps {
container: Explorer;
@@ -27,7 +27,6 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
private static readonly ButtonText = "Add";
private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch";
private static readonly TextFieldErrorMessage = "Invalid url";
private static readonly DefaultBranchName = "master";
constructor(props: AddRepoComponentProps) {
super(props);
@@ -78,7 +77,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
});
let enteredUrl = this.state.textFieldValue;
if (enteredUrl.indexOf("/tree/") === -1) {
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/${AddRepoComponent.DefaultBranchName}`);
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/`);
}
const repoInfo = GitHubUtils.fromRepoUri(enteredUrl);
@@ -93,11 +92,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
const item: RepoListItem = {
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
repo,
branches: [
{
name: repoInfo.branch,
},
],
branches: repoInfo.branch ? [{ name: repoInfo.branch }] : [],
};
TelemetryProcessor.traceSuccess(

View File

@@ -24,11 +24,11 @@ import { RepoListItem } from "./GitHubReposComponent";
import {
BranchesDropdownCheckboxStyles,
BranchesDropdownOptionContainerStyle,
BranchesDropdownStyles,
BranchesDropdownWidth,
ReposListBranchesColumnWidth,
ReposListCheckboxStyles,
ReposListRepoColumnMinWidth,
ReposListBranchesColumnWidth,
BranchesDropdownWidth,
BranchesDropdownStyles,
} from "./GitHubStyleConstants";
export interface ReposListComponentProps {
@@ -44,6 +44,7 @@ export interface BranchesProps {
lastPageInfo?: IGitHubPageInfo;
hasMore: boolean;
isLoading: boolean;
defaultBranchName: string;
loadMore: () => void;
}
@@ -64,7 +65,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
private static readonly BranchesColumnName = "Branches";
private static readonly LoadingText = "Loading...";
private static readonly LoadMoreText = "Load more";
private static readonly DefaultBranchName = "master";
private static readonly DefaultBranchNames = "master/main";
private static readonly FooterIndex = -1;
public render(): JSX.Element {
@@ -155,6 +156,10 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
}
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
if (item.branches.length === 0 && branchesProps.defaultBranchName) {
item.branches = [{ name: branchesProps.defaultBranchName }];
}
const options: IDropdownOption[] = branchesProps.branches.map((branch) => ({
key: branch.name,
text: branch.name,
@@ -198,7 +203,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
const dropdownProps: IDropdownProps = {
styles: BranchesDropdownStyles,
options: [],
placeholder: ReposListComponent.DefaultBranchName,
placeholder: ReposListComponent.DefaultBranchNames,
disabled: true,
};
@@ -272,7 +277,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
styles: ReposListCheckboxStyles,
onChange: () => {
const repoListItem = { ...item };
repoListItem.branches = [{ name: ReposListComponent.DefaultBranchName }];
repoListItem.branches = [];
this.props.pinRepo(repoListItem);
},
};

View File

@@ -35,16 +35,19 @@ const testCassandraAccount: DataModels.DatabaseAccount = {
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
forwardingId: "Id",
};
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
forwardingId: "Id",
};
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
forwardingId: "Id",
};
describe("NotebookTerminalComponent", () => {

View File

@@ -17,7 +17,6 @@ import Explorer from "../../Explorer";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import { useNotebook } from "../../Notebook/useNotebook";
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
@@ -53,7 +52,7 @@ export class NotebookViewerComponent
super(props);
this.clientManager = new NotebookClientV2({
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined },
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined, forwardingId: undefined },
databaseAccountName: undefined,
defaultExperience: "NotebookViewer",
isReadOnly: true,
@@ -148,9 +147,7 @@ export class NotebookViewerComponent
<NotebookMetadataComponent
data={this.state.galleryItem}
isFavorite={this.state.isFavorite}
downloadButtonText={
this.props.container && NotebookUtil.getNotebookBtnTitle(useNotebook.getState().notebookFolderName)
}
downloadButtonText={this.props.container && `Download to ${useNotebook.getState().notebookFolderName}`}
onTagClick={this.props.onTagClick}
onFavoriteClick={this.favoriteItem}
onUnfavoriteClick={this.unfavoriteItem}

View File

@@ -1,4 +1,5 @@
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
import { useDatabases } from "Explorer/useDatabases";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
@@ -71,6 +72,7 @@ export interface SettingsComponentState {
wasAutopilotOriginallySet: boolean;
isScaleSaveable: boolean;
isScaleDiscardable: boolean;
throughputError: string;
timeToLive: TtlType;
timeToLiveBaseline: TtlType;
@@ -124,6 +126,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean;
private shouldShowIndexingPolicyEditor: boolean;
private totalThroughputUsed: number;
public mongoDBCollectionResource: MongoDBCollectionResource;
constructor(props: SettingsComponentProps) {
@@ -155,6 +158,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
wasAutopilotOriginallySet: false,
isScaleSaveable: false,
isScaleDiscardable: false,
throughputError: undefined,
timeToLive: undefined,
timeToLiveBaseline: undefined,
@@ -208,6 +212,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return true;
},
};
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
this.calculateTotalThroughputUsed();
}
}
componentDidMount(): void {
@@ -254,6 +263,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return false;
}
if (this.state.throughputError) {
return false;
}
return (
this.state.isScaleSaveable ||
this.state.isSubSettingsSaveable ||
@@ -481,6 +494,26 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
this.setState({ isMongoIndexingPolicyDiscardable });
private calculateTotalThroughputUsed = (): void => {
this.totalThroughputUsed = 0;
(useDatabases.getState().databases || []).forEach(async (database) => {
if (database.offer()) {
const dbThroughput = database.offer().autoscaleMaxThroughput || database.offer().manualThroughput;
this.totalThroughputUsed += dbThroughput;
}
(database.collections() || []).forEach(async (collection) => {
if (collection.offer()) {
const colThroughput = collection.offer().autoscaleMaxThroughput || collection.offer().manualThroughput;
this.totalThroughputUsed += colThroughput;
}
});
});
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
this.totalThroughputUsed *= numberOfRegions;
};
public getAnalyticalStorageTtl = (): number => {
if (this.isAnalyticalStorageEnabled) {
if (this.state.analyticalStorageTtlSelection === TtlType.On) {
@@ -643,10 +676,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return buttons;
};
private onMaxAutoPilotThroughputChange = (newThroughput: number): void =>
this.setState({ autoPilotThroughput: newThroughput });
private onMaxAutoPilotThroughputChange = (newThroughput: number): void => {
let throughputError = "";
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
this.totalThroughputUsed + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
}
this.setState({ autoPilotThroughput: newThroughput, throughputError });
};
private onThroughputChange = (newThroughput: number): void => this.setState({ throughput: newThroughput });
private onThroughputChange = (newThroughput: number): void => {
let throughputError = "";
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
this.totalThroughputUsed + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
}
this.setState({ throughput: newThroughput, throughputError });
};
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
@@ -893,6 +947,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onScaleSaveableChange: this.onScaleSaveableChange,
onScaleDiscardableChange: this.onScaleDiscardableChange,
initialNotification: this.props.settingsTab.pendingNotification(),
throughputError: this.state.throughputError,
};
if (!this.isCollectionSettingsTab) {

View File

@@ -36,6 +36,7 @@ export interface ScaleComponentProps {
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
initialNotification: DataModels.Notification;
throughputError?: string;
}
export class ScaleComponent extends React.Component<ScaleComponentProps> {
@@ -189,6 +190,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage}
usageSizeInKB={this.props.collection?.usageSizeInKB()}
throughputError={this.props.throughputError}
/>
);

View File

@@ -75,6 +75,7 @@ export interface ThroughputInputAutoPilotV3Props {
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
getThroughputWarningMessage: () => JSX.Element;
usageSizeInKB: number;
throughputError?: string;
}
interface ThroughputInputAutoPilotV3State {
@@ -540,6 +541,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
min={minAutoPilotThroughput}
errorMessage={this.props.throughputError}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
{this.minRUperGBSurvey()}
@@ -579,6 +581,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
}
onChange={this.onThroughputChange}
min={this.props.minimum}
errorMessage={this.props.throughputError}
/>
{this.state.exceedFreeTierThroughput && (
<MessageBar

View File

@@ -34,7 +34,13 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"phoenixClient": PhoenixClient {
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -102,7 +108,13 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"phoenixClient": PhoenixClient {
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],

View File

@@ -7,6 +7,7 @@ const props = {
isSharded: true,
setThroughputValue: () => jest.fn(),
setIsAutoscale: () => jest.fn(),
setIsThroughputCapExceeded: () => jest.fn(),
onCostAcknowledgeChange: () => jest.fn(),
};
describe("ThroughputInput Pane", () => {

View File

@@ -1,5 +1,6 @@
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
import React, { FunctionComponent, useState } from "react";
import { useDatabases } from "Explorer/useDatabases";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import * as SharedConstants from "../../../Shared/Constants";
@@ -16,6 +17,7 @@ export interface ThroughputInputProps {
showFreeTierExceedThroughputTooltip: boolean;
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
}
@@ -24,6 +26,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
showFreeTierExceedThroughputTooltip,
setThroughputValue,
setIsAutoscale,
setIsThroughputCapExceeded,
isSharded,
onCostAcknowledgeChange,
}: ThroughputInputProps) => {
@@ -31,10 +34,60 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>("");
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
setIsAutoscale(isAutoscaleSelected);
setThroughputValue(throughput);
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
useEffect(() => {
// throughput cap check for the initial state
let totalThroughput = 0;
(useDatabases.getState().databases || []).forEach((database) => {
if (database.offer()) {
const dbThroughput = database.offer().autoscaleMaxThroughput || database.offer().manualThroughput;
totalThroughput += dbThroughput;
}
(database.collections() || []).forEach((collection) => {
if (collection.offer()) {
const colThroughput = collection.offer().autoscaleMaxThroughput || collection.offer().manualThroughput;
totalThroughput += colThroughput;
}
});
});
totalThroughput *= numberOfRegions;
setTotalThroughputUsed(totalThroughput);
if (throughputCap && throughputCap !== -1 && throughputCap - totalThroughput < throughput) {
setThroughputError(
`Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
totalThroughput + throughput * numberOfRegions
} RU/s. Change total throughput limit in cost management.`
);
setIsThroughputCapExceeded(true);
}
}, []);
const checkThroughputCap = (newThroughput: number): boolean => {
if (throughputCap && throughputCap !== -1 && throughputCap - totalThroughputUsed < newThroughput) {
setThroughputError(
`Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
totalThroughputUsed + newThroughput * numberOfRegions
} RU/s. Change total throughput limit in cost management.`
);
setIsThroughputCapExceeded(true);
return false;
}
setThroughputError("");
setIsThroughputCapExceeded(false);
return true;
};
const getThroughputLabelText = (): string => {
let throughputHeaderText: string;
if (isAutoscaleSelected) {
@@ -60,11 +113,17 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const newThroughput = parseInt(newInput);
setThroughput(newThroughput);
setThroughputValue(newThroughput);
if (!isSharded && newThroughput > 10000) {
setThroughputError("Unsharded collections support up to 10,000 RUs");
} else {
setThroughputError("");
return;
}
if (!checkThroughputCap(newThroughput)) {
return;
}
setThroughputError("");
};
const getAutoScaleTooltip = (): string => {
@@ -96,11 +155,13 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
setIsAutoScaleSelected(true);
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoscale(true);
checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput);
} else {
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoScaleSelected(false);
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoscale(false);
checkThroughputCap(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
}
};

View File

@@ -6,6 +6,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setIsThroughputCapExceeded={[Function]}
setThroughputValue={[Function]}
showFreeTierExceedThroughputTooltip={true}
>

View File

@@ -1,19 +1,25 @@
import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import * as ko from "knockout";
import React from "react";
import _ from "underscore";
import shallow from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
import { ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient";
import * as DataModels from "../Contracts/DataModels";
import { ContainerConnectionInfo } from "../Contracts/DataModels";
import {
ContainerConnectionInfo,
IPhoenixConnectionInfoResult,
IProvisionData,
IResponse,
} from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel";
@@ -29,7 +35,6 @@ import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import {
get as getWorkspace,
listByDatabaseAccount,
listConnectionInfo,
start,
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { stringToBlob } from "../Utils/BlobUtils";
@@ -165,11 +170,9 @@ export default class Explorer {
);
useNotebook.subscribe(
async () => {
this.initiateAndRefreshNotebookList();
useNotebook.getState().setIsRefreshed(false);
},
(state) => state.isNotebookEnabled || state.isRefreshed
async () => this.initiateAndRefreshNotebookList(),
(state) => [state.isNotebookEnabled, state.isRefreshed],
shallow
);
this.resourceTree = new ResourceTreeAdapter(this);
@@ -179,6 +182,7 @@ export default class Explorer {
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl,
authToken: userContext.features.notebookServerToken,
forwardingId: undefined,
});
}
@@ -352,35 +356,19 @@ export default class Explorer {
return;
}
this._isInitializingNotebooks = true;
if (userContext.features.phoenix === false) {
await this.ensureNotebookWorkspaceRunning();
const connectionInfo = await listConnectionInfo(
userContext.subscriptionId,
userContext.resourceGroup,
databaseAccount.name,
"default"
);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
});
}
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
public async allocateContainer(): Promise<void> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const isAllocating = useNotebook.getState().isAllocating;
if (isAllocating === false && notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined) {
const provisionData = {
aadToken: userContext.authorizationToken,
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
if (
isAllocating === false &&
(notebookServerInfo === undefined ||
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
};
const connectionStatus: ContainerConnectionInfo = {
@@ -388,38 +376,61 @@ export default class Explorer {
};
useNotebook.getState().setConnectionInfo(connectionStatus);
try {
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
useNotebook.getState().setIsAllocating(true);
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
if (
connectionInfo.status === HttpStatusCodes.OK &&
connectionInfo.data &&
connectionInfo.data.notebookServerUrl
) {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
});
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
useNotebook.getState().setIsAllocating(false);
} else {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetConatinerConnection(connectionStatus);
const connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
if (connectionInfo.status !== HttpStatusCodes.OK) {
throw new Error(`Received status code: ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.notebookServerUrl) {
throw new Error(`NotebookServerUrl is invalid!`);
}
await this.setNotebookInfo(connectionInfo, connectionStatus);
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
TelemetryProcessor.traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().resetContainerConnection(connectionStatus);
throw error;
} finally {
useNotebook.getState().setIsAllocating(false);
this.refreshCommandBarButtons();
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
}
private async setNotebookInfo(
connectionInfo: IResponse<IPhoenixConnectionInfoResult>,
connectionStatus: DataModels.ContainerConnectionInfo
) {
const containerData = {
forwardingId: connectionInfo.data.forwardingId,
dbAccountName: userContext.databaseAccount.name,
};
await this.phoenixClient.initiateContainerHeartBeat(containerData);
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
forwardingId: connectionInfo.data.forwardingId,
});
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
}
public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError(
@@ -428,11 +439,14 @@ export default class Explorer {
);
return;
}
const dialogContent = useNotebook.getState().isPhoenix
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
const resetConfirmationDialogProps: DialogProps = {
isModal: true,
title: "Reset Workspace",
subText: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?",
subText: dialogContent,
primaryButtonText: "OK",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: this._resetNotebookWorkspace,
@@ -490,16 +504,54 @@ export default class Explorer {
private _resetNotebookWorkspace = async () => {
useDialog.getState().closeDialog();
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
let connectionStatus: ContainerConnectionInfo;
try {
await this.notebookManager?.notebookClient.resetWorkspace();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
logConsoleError(error);
return;
}
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
if (useNotebook.getState().isPhoenix) {
useTabs.getState().closeAllNotebookTabs(true);
connectionStatus = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
}
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
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 (useNotebook.getState().isPhoenix) {
await this.setNotebookInfo(connectionInfo, connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
logConsoleInfo("Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
logConsoleError(`Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
if (useNotebook.getState().isPhoenix) {
connectionStatus = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
throw error;
} finally {
clearInProgressMessage();
@@ -691,8 +743,8 @@ export default class Explorer {
if (!notebookContentItem || !notebookContentItem.path) {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
}
if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) {
this.allocateContainer();
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenix) {
await this.allocateContainer();
}
const notebookTabs = useTabs
@@ -915,8 +967,7 @@ export default class Explorer {
handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error);
}
const isPhoenixEnabled = NotebookUtil.isPhoenixEnabled();
if (isPhoenixEnabled) {
if (useNotebook.getState().isPhoenix) {
if (isGithubTree) {
async () => {
await this.allocateContainer();
@@ -1007,7 +1058,7 @@ export default class Explorer {
}
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (NotebookUtil.isPhoenixEnabled()) {
if (useNotebook.getState().isPhoenix) {
await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
@@ -1016,8 +1067,8 @@ export default class Explorer {
useDialog
.getState()
.showOkModalDialog(
"Failed to Connect",
"Failed to connect temporary workspace, this could happen because of network issue please refresh and try again."
"Failed to connect",
"Failed to connect to temporary workspace. This could happen because of network issues. Please refresh the page and try again."
);
}
} else {
@@ -1119,7 +1170,10 @@ export default class Explorer {
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
);
} else {
await useDatabases.getState().loadDatabaseOffers();
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
throughputCap && throughputCap !== -1
? await useDatabases.getState().loadAllOffers()
: await useDatabases.getState().loadDatabaseOffers();
useSidePanel
.getState()
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
@@ -1145,10 +1199,9 @@ export default class Explorer {
}
public async handleOpenFileAction(path: string): Promise<void> {
if (
userContext.features.phoenix === false &&
!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))
) {
if (useNotebook.getState().isPhoenix) {
await this.allocateContainer();
} else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
this._openSetupNotebooksPaneForQuickstart();
}
@@ -1180,7 +1233,7 @@ export default class Explorer {
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
if (NotebookUtil.isPhoenixEnabled()) {
if (useNotebook.getState().isPhoenix) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
@@ -1210,7 +1263,7 @@ export default class Explorer {
}
public getDownloadModalConent(fileName: string): JSX.Element {
if (NotebookUtil.isPhoenixEnabled()) {
if (useNotebook.getState().isPhoenix) {
return (
<>
<p>{Notebook.galleryNotebookDownloadContent1}</p>
@@ -1232,28 +1285,23 @@ export default class Explorer {
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
let isNotebookEnabled = true;
if (!userContext.features.phoenix) {
isNotebookEnabled =
userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
userContext.features.enableNotebooks);
//Disable phoenix in case of Vnet or Firewall was enabled.
if (!isPublicInternetAccessAllowed()) {
useNotebook.getState().setIsPhoenix(false);
}
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenix;
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
useNotebook.getState().setIsShellEnabled(useNotebook.getState().isPhoenix && isPublicInternetAccessAllowed());
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled,
dataExplorerArea: Constants.Areas.Notebook,
});
if (!userContext.features.notebooksTemporarilyDown) {
if (isNotebookEnabled) {
await this.initNotebooks(userContext.databaseAccount);
} else if (this.notebookToImport) {
// if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane
this._openSetupNotebooksPaneForQuickstart();
}
if (useNotebook.getState().isPhoenix) {
await this.initNotebooks(userContext.databaseAccount);
}
}
}

View File

@@ -4,15 +4,12 @@
* and update any knockout observables passed from the parent.
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import * as React from "react";
import create, { UseStore } from "zustand";
import { StyleConstants } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import { userContext } from "../../../UserContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
@@ -56,18 +53,10 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (NotebookUtil.isPhoenixEnabled()) {
if (useNotebook.getState().isPhoenix) {
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
}
if (
userContext.features.phoenix === false &&
userContext.features.notebooksTemporarilyDown === false &&
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
}
return (
<div className="commandBarContainer">
<FluentCommandBar

View File

@@ -78,9 +78,10 @@ export function createStaticCommandBarButtons(
if (container.notebookManager?.gitHubOAuthService) {
notebookButtons.push(createManageGitHubAccountButton(container));
}
notebookButtons.push(createOpenTerminalButton(container));
if (userContext.features.phoenix === false) {
if (useNotebook.getState().isPhoenix && configContext.IS_MPAC) {
notebookButtons.push(createOpenTerminalButton(container));
}
if (selectedNodeState.isConnectedToContainer()) {
notebookButtons.push(createNotebookWorkspaceResetButton(container));
}
if (
@@ -98,7 +99,7 @@ export function createStaticCommandBarButtons(
}
notebookButtons.forEach((btn) => {
if (userContext.features.notebooksTemporarilyDown) {
if (!useNotebook.getState().isPhoenix) {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
@@ -110,7 +111,7 @@ export function createStaticCommandBarButtons(
buttons.push(btn);
});
} else {
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenix) {
buttons.push(createDivider());
buttons.push(createEnableNotebooksButton(container));
}
@@ -168,7 +169,7 @@ export function createContextCommandBarButtons(
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
if (!userContext.features.notebooksTemporarilyDown) {
if (useNotebook.getState().isPhoenix) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
} else {
@@ -179,12 +180,12 @@ export function createContextCommandBarButtons(
ariaLabel: label,
hasPopup: true,
tooltipText:
useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown
useNotebook.getState().isShellEnabled && !useNotebook.getState().isPhoenix
? Constants.Notebook.mongoShellTemporarilyDownMsg
: undefined,
disabled:
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
(useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown),
(useNotebook.getState().isShellEnabled && !useNotebook.getState().isPhoenix),
};
buttons.push(newMongoShellBtn);
}
@@ -307,18 +308,16 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
function createNewDatabase(container: Explorer): CommandButtonComponentProps {
const label = "New " + getDatabaseName();
const newDatabaseButton = document.activeElement as HTMLElement;
return {
iconSrc: AddDatabaseIcon,
iconAlt: label,
onCommandClick: () =>
useSidePanel
.getState()
.openSidePanel(
"New " + getDatabaseName(),
<AddDatabasePanel explorer={container} buttonElement={newDatabaseButton} />
),
onCommandClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
await useDatabases.getState().loadAllOffers();
}
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,

View File

@@ -1,8 +1,20 @@
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
import { ActionButton } from "@fluentui/react/lib/Button";
import {
FocusTrapCallout,
FocusZone,
FocusZoneTabbableElements,
FontWeights,
Icon,
mergeStyleSets,
ProgressIndicator,
Stack,
Text,
TooltipHost,
} from "@fluentui/react";
import { useId } from "@fluentui/react-hooks";
import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button";
import * as React from "react";
import "../../../../less/hostedexplorer.less";
import { ConnectionStatusType, Notebook } from "../../../Common/Constants";
import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less";
@@ -10,12 +22,33 @@ interface Props {
container: Explorer;
}
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
const connectionInfo = useNotebook((state) => state.connectionInfo);
const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00");
const [isActive, setIsActive] = React.useState(false);
const [counter, setCounter] = React.useState(0);
const [statusColor, setStatusColor] = React.useState("");
const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
const [isBarDismissed, setIsBarDismissed] = React.useState<boolean>(false);
const buttonId = useId("callout-button");
const containerInfo = useNotebook((state) => state.containerStatus);
const styles = mergeStyleSets({
callout: {
width: 320,
padding: "20px 24px",
},
title: {
marginBottom: 12,
fontWeight: FontWeights.semilight,
},
buttons: {
display: "flex",
justifyContent: "flex-end",
marginTop: 20,
},
});
React.useEffect(() => {
let intervalId: NodeJS.Timeout;
@@ -35,6 +68,15 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
return () => clearInterval(intervalId);
}, [isActive, counter]);
React.useEffect(() => {
if (connectionInfo?.status === ConnectionStatusType.Reconnect) {
setToolTipContent("Click here to Reconnect to temporary workspace.");
} else if (connectionInfo?.status === ConnectionStatusType.Failed) {
setStatusColor("status failed is-animating");
setToolTipContent("Click here to Reconnect to temporary workspace.");
}
}, [connectionInfo.status]);
const stopTimer = () => {
setIsActive(false);
setCounter(0);
@@ -42,15 +84,13 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
setMinute("00");
};
const connectionInfo = useNotebook((state) => state.connectionInfo);
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
if (
connectionInfo &&
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.ReConnect)
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
) {
return (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
@@ -65,6 +105,7 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
}
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
stopTimer();
setIsActive(true);
setStatusColor("status connecting is-animating");
setToolTipContent("Connecting to temporary workspace.");
@@ -78,30 +119,68 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
setToolTipContent("Click here to Reconnect to temporary workspace.");
}
return (
<ActionButton
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
}
>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<i className={statusColor}></i>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
)}
</Stack>
<>
<TooltipHost
content={
containerInfo?.status === ContainerStatusType.Active
? `Connected to temporary workspace. This temporary workspace will get disconnected in ${Math.round(
containerInfo.durationLeftInMinutes
)} minutes.`
: toolTipContent
}
>
<ActionButton
id={buttonId}
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
}
>
<Stack className="connectionStatusContainer" horizontal>
<i className={statusColor}></i>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
<ProgressIndicator
className={totalGB !== 0 && usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={totalGB !== 0 ? usedGB / totalGB : 0}
/>
)}
</Stack>
{!isBarDismissed &&
containerInfo.status &&
containerInfo.status === ContainerStatusType.Active &&
Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert ? (
<FocusTrapCallout
role="alertdialog"
className={styles.callout}
gapSpace={0}
target={`#${buttonId}`}
onDismiss={() => setIsBarDismissed(true)}
setInitialFocus
>
<Text block variant="xLarge" className={styles.title}>
Remaining Time
</Text>
<Text block variant="small">
This temporary workspace will get disconnected in {Math.round(containerInfo.durationLeftInMinutes)}{" "}
minutes. To save your work permanently, save your notebooks to a GitHub repository or download the
notebooks to your local machine before the session ends.
</Text>
<FocusZone handleTabKey={FocusZoneTabbableElements.all} isCircularNavigation>
<Stack className={styles.buttons} gap={8} horizontal>
<DefaultButton onClick={() => setIsBarDismissed(true)}>Dimiss</DefaultButton>
</Stack>
</FocusZone>
</FocusTrapCallout>
) : undefined}
</ActionButton>
</TooltipHost>
</ActionButton>
</>
);
};

View File

@@ -1,23 +1,32 @@
/**
* Notebook container related stuff
*/
import promiseRetry, { AbortError } from "p-retry";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType } from "../../Common/Constants";
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
import { IPhoenixConnectionInfoResult, IProvisionData, IResponse } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { NotebookUtil } from "./NotebookUtil";
import { useNotebook } from "./useNotebook";
export class NotebookContainerClient {
private clearReconnectionAttemptMessage? = () => {};
private isResettingWorkspace: boolean;
private phoenixClient: PhoenixClient;
private retryOptions: promiseRetry.Options;
constructor(private onConnectionLost: () => void) {
this.phoenixClient = new PhoenixClient();
this.retryOptions = {
retries: Notebook.retryAttempts,
maxTimeout: Notebook.retryAttemptDelayMs,
minTimeout: Notebook.retryAttemptDelayMs,
};
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
@@ -38,10 +47,13 @@ export class NotebookContainerClient {
* Heartbeat: each ping schedules another ping
*/
private scheduleHeartbeat(delayMs: number): void {
setTimeout(() => {
this.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
setTimeout(async () => {
const memoryUsageInfo = await this.getMemoryUsage();
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
}, delayMs);
}
@@ -59,6 +71,27 @@ export class NotebookContainerClient {
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try {
const runMemoryAsync = async () => {
return await this._getMemoryAsync(notebookServerEndpoint, authToken);
};
return await promiseRetry(runMemoryAsync, this.retryOptions);
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
if (!this.clearReconnectionAttemptMessage) {
this.clearReconnectionAttemptMessage = logConsoleProgress(
"Connection lost with Notebook server. Attempting to reconnect..."
);
}
this.onConnectionLost();
return undefined;
}
}
private async _getMemoryAsync(
notebookServerEndpoint: string,
authToken: string
): Promise<DataModels.MemoryUsageInfo> {
if (this.shouldExecuteMemoryCall()) {
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
method: "GET",
headers: {
@@ -78,44 +111,39 @@ export class NotebookContainerClient {
freeKB: memoryUsageInfo.free,
};
}
} else if (NotebookUtil.isPhoenixEnabled()) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.ReConnect,
};
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(true);
} else if (response.status === HttpStatusCodes.NotFound) {
throw new AbortError(response.statusText);
}
return undefined;
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
if (!this.clearReconnectionAttemptMessage) {
this.clearReconnectionAttemptMessage = logConsoleProgress(
"Connection lost with Notebook server. Attempting to reconnect..."
);
}
if (NotebookUtil.isPhoenixEnabled()) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(true);
}
this.onConnectionLost();
throw new Error(response.statusText);
} else {
return undefined;
}
}
public async resetWorkspace(): Promise<void> {
private shouldExecuteMemoryCall(): boolean {
if (
useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active &&
useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected
) {
return true;
}
return false;
}
public async resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
this.isResettingWorkspace = true;
let response: IResponse<IPhoenixConnectionInfoResult>;
try {
await this._resetWorkspace();
response = await this._resetWorkspace();
} catch (error) {
Promise.reject(error);
return response;
}
this.isResettingWorkspace = false;
return response;
}
private async _resetWorkspace(): Promise<void> {
private async _resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
@@ -123,15 +151,17 @@ export class NotebookContainerClient {
return Promise.reject(error);
}
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try {
await fetch(`${notebookServerEndpoint}/api/shutdown`, {
method: "POST",
headers: { Authorization: authToken },
});
if (useNotebook.getState().isPhoenix) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
};
return await this.phoenixClient.resetContainer(provisionData);
}
return null;
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
await this.recreateNotebookWorkspaceAsync();
throw error;
}
}
@@ -145,22 +175,11 @@ export class NotebookContainerClient {
};
}
private async recreateNotebookWorkspaceAsync(): Promise<void> {
const { databaseAccount } = userContext;
if (!databaseAccount?.id) {
throw new Error("DataExplorer not initialized");
}
try {
await destroy(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
await createOrUpdate(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
"default"
);
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync");
return Promise.reject(error);
}
private getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader();
return {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
};
}
}

View File

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

View File

@@ -35,6 +35,7 @@ describe("auto start kernel", () => {
connectionInfo: {
authToken: "autToken",
notebookServerEndpoint: "notebookServerEndpoint",
forwardingId: "Id",
},
databaseAccountName: undefined,
defaultExperience: undefined,

View File

@@ -1,4 +1,5 @@
import { cloneDeep } from "lodash";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
@@ -7,7 +8,8 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
import { useTabs } from "../../hooks/useTabs";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -16,7 +18,6 @@ import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import NotebookManager from "./NotebookManager";
import { NotebookUtil } from "./NotebookUtil";
interface NotebookState {
isNotebookEnabled: boolean;
@@ -35,6 +36,8 @@ interface NotebookState {
notebookFolderName: string;
isAllocating: boolean;
isRefreshed: boolean;
containerStatus: ContainerInfo;
isPhoenix: boolean;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -53,8 +56,11 @@ interface NotebookState {
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
setIsAllocating: (isAllocating: boolean) => void;
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo) => void;
resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void;
setIsRefreshed: (isAllocating: boolean) => void;
setContainerStatus: (containerStatus: ContainerInfo) => void;
getPhoenixStatus: () => Promise<void>;
setIsPhoenix: (isPhoenix: boolean) => void;
}
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -63,6 +69,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
},
sparkClusterConnectionInfo: {
userName: undefined,
@@ -83,6 +90,12 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
notebookFolderName: undefined,
isAllocating: false,
isRefreshed: false,
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
},
isPhoenix: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -95,6 +108,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
await get().getPhoenixStatus();
const { databaseAccount, authType } = userContext;
if (
authType === AuthType.EncryptedToken ||
@@ -187,7 +201,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = NotebookUtil.isPhoenixEnabled() === true ? "Temporary Notebooks" : "My Notebooks";
const notebookFolderName = get().isPhoenix === true ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName });
const myNotebooksContentRoot = {
name: get().notebookFolderName,
@@ -270,13 +284,25 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
},
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo): void => {
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
useTabs.getState().closeAllNotebookTabs(true);
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: undefined,
authToken: undefined,
});
useNotebook.getState().setNotebookServerInfo(undefined);
useNotebook.getState().setIsAllocating(false);
useNotebook.getState().setContainerStatus({
status: undefined,
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
});
},
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
getPhoenixStatus: async () => {
if (get().isPhoenix === undefined) {
const phoenixClient = new PhoenixClient();
const isPhoenix = userContext.features.phoenix === true && (await phoenixClient.IsDbAcountWhitelisted());
set({ isPhoenix });
}
},
setIsPhoenix: (isPhoenix: boolean) => set({ isPhoenix }),
}));

View File

@@ -92,6 +92,7 @@ export interface AddCollectionPanelState {
errorMessage: string;
showErrorDetails: boolean;
isExecuting: boolean;
isThroughputCapExceeded: boolean;
}
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
@@ -122,6 +123,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
errorMessage: "",
showErrorDetails: false,
isExecuting: false,
isThroughputCapExceeded: false,
};
}
@@ -249,6 +251,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded })
}
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
/>
)}
@@ -480,6 +485,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded })
}
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
this.isCostAcknowledged = isAcknowledged;
}}
@@ -676,7 +684,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
</div>
<PanelFooterComponent buttonLabel="OK" />
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
{this.state.isExecuting && <PanelLoadingScreen />}
</form>

View File

@@ -52,6 +52,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
);
const [formErrors, setFormErrors] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false);
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
@@ -79,7 +80,9 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
dataExplorerArea: Constants.Areas.ContextualPane,
};
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
buttonElement.focus();
if (buttonElement) {
buttonElement.focus();
}
}, []);
const onSubmit = () => {
@@ -169,6 +172,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
formError: formErrors,
isExecuting,
submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
@@ -239,6 +243,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
isSharded={databaseCreateNewShared}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}

View File

@@ -4,6 +4,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
<RightPaneForm
formError=""
isExecuting={false}
isSubmitButtonDisabled={false}
onSubmit={[Function]}
submitButtonText="OK"
>
@@ -92,6 +93,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setIsThroughputCapExceeded={[Function]}
setThroughputValue={[Function]}
/>
</div>

View File

@@ -43,6 +43,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false);
const [isExecuting, setIsExecuting] = useState<boolean>();
const [formError, setFormError] = useState<string>("");
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false);
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const addCollectionPaneOpenMessage = {
@@ -149,6 +150,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
formError,
isExecuting,
submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
@@ -262,6 +264,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
isSharded
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}
@@ -334,6 +337,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
isSharded={false}
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}

View File

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

View File

@@ -369,18 +369,21 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</div>
<PanelFooterComponent
buttonLabel="OK"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
type="submit"
>
<PrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
theme={
@@ -660,6 +663,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<CustomizedDefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -941,6 +945,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<DefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1223,6 +1228,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -1,6 +1,6 @@
import { IDropdownOption, IImageProps, Image, Stack, Text } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react";
import React, { FunctionComponent, useRef, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
@@ -25,19 +25,16 @@ interface UnwrappedExecuteSprocParam {
export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
storedProcedure,
}: ExecuteSprocParamsPaneProps): JSX.Element => {
const paramKeyValuesRef = useRef<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
const partitionValueRef = useRef<string>();
const partitionKeyRef = useRef<string>("string");
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [numberOfParams, setNumberOfParams] = useState<number>(1);
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [paramKeyValues, setParamKeyValues] = useState<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
const [partitionValue, setPartitionValue] = useState<string>(); // Defaulting to undefined here is important. It is not the same partition key as ""
const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
const [formError, setFormError] = useState<string>("");
const onPartitionKeyChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
setSelectedKey(item);
};
const validateUnwrappedParams = (): boolean => {
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValuesRef.current;
for (let i = 0; i < unwrappedParams.length; i++) {
const { key: paramType, text: paramValue } = unwrappedParams[i];
if (paramType === "custom" && (paramValue === "" || paramValue === undefined)) {
@@ -53,8 +50,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
};
const submit = (): void => {
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
const { key: partitionKey } = selectedKey;
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValuesRef.current;
const partitionValue: string = partitionValueRef.current;
const partitionKey: string = partitionKeyRef.current;
if (partitionKey === "custom" && (partitionValue === "" || partitionValue === undefined)) {
setInvalidParamError(partitionValue);
return;
@@ -78,37 +76,21 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
};
const deleteParamAtIndex = (indexToRemove: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(indexToRemove, 1);
setParamKeyValues(cloneParamKeyValue);
paramKeyValuesRef.current.splice(indexToRemove, 1);
setNumberOfParams(numberOfParams - 1);
};
const addNewParamAtIndex = (indexToAdd: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(indexToAdd, 0, { key: "string", text: "" });
setParamKeyValues(cloneParamKeyValue);
};
const paramValueChange = (value: string, indexOfInput: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue[indexOfInput].text = value;
setParamKeyValues(cloneParamKeyValue);
};
const paramKeyChange = (
_event: React.FormEvent<HTMLDivElement>,
selectedParam: IDropdownOption,
indexOfParam: number
): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue[indexOfParam].key = selectedParam.key.toString();
setParamKeyValues(cloneParamKeyValue);
paramKeyValuesRef.current.splice(indexToAdd, 0, { key: "string", text: "" });
setNumberOfParams(numberOfParams + 1);
};
const addNewParamAtLastIndex = (): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(cloneParamKeyValue.length, 0, { key: "string", text: "" });
setParamKeyValues(cloneParamKeyValue);
paramKeyValuesRef.current.push({
key: "string",
text: "",
});
setNumberOfParams(numberOfParams + 1);
};
const props: RightPaneFormProps = {
@@ -118,46 +100,52 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
onSubmit: () => submit(),
};
const getInputParameterComponent = (): JSX.Element[] => {
const inputParameters: JSX.Element[] = [];
for (let i = 0; i < numberOfParams; i++) {
const paramKeyValue = paramKeyValuesRef.current[i];
inputParameters.push(
<InputParameter
key={paramKeyValue.text + i}
dropdownLabel={i === 0 ? "Key" : ""}
inputParameterTitle={i === 0 ? "Enter input parameters (if any)" : ""}
inputLabel={i === 0 ? "Param" : ""}
isAddRemoveVisible={true}
onDeleteParamKeyPress={() => deleteParamAtIndex(i)}
onAddNewParamKeyPress={() => addNewParamAtIndex(i + 1)}
onParamValueChange={(_event, newInput?: string) => (paramKeyValuesRef.current[i].text = newInput)}
onParamKeyChange={(_event, selectedParam: IDropdownOption) =>
(paramKeyValuesRef.current[i].key = selectedParam.key.toString())
}
paramValue={paramKeyValue.text}
selectedKey={paramKeyValue.key}
/>
);
}
return inputParameters;
};
return (
<RightPaneForm {...props}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<InputParameter
dropdownLabel="Key"
inputParameterTitle="Partition key value"
inputLabel="Value"
isAddRemoveVisible={false}
onParamValueChange={(_event, newInput?: string) => {
setPartitionValue(newInput);
}}
onParamKeyChange={onPartitionKeyChange}
paramValue={partitionValue}
selectedKey={selectedKey.key}
/>
{paramKeyValues.map((paramKeyValue, index) => (
<InputParameter
key={paramKeyValue && paramKeyValue.text + index}
dropdownLabel={!index && "Key"}
inputParameterTitle={!index && "Enter input parameters (if any)"}
inputLabel={!index && "Param"}
isAddRemoveVisible={true}
onDeleteParamKeyPress={() => deleteParamAtIndex(index)}
onAddNewParamKeyPress={() => addNewParamAtIndex(index + 1)}
onParamValueChange={(event, newInput?: string) => {
paramValueChange(newInput, index);
}}
onParamKeyChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
paramKeyChange(event, selectedParam, index);
}}
paramValue={paramKeyValue && paramKeyValue.text}
selectedKey={paramKeyValue && paramKeyValue.key}
/>
))}
<Stack horizontal onClick={addNewParamAtLastIndex} tabIndex={0}>
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
<Text className="addNewParamStyle">Add New Param</Text>
</Stack>
</div>
<div className="panelMainContent">
<InputParameter
dropdownLabel="Key"
inputParameterTitle="Partition key value"
inputLabel="Value"
isAddRemoveVisible={false}
onParamValueChange={(_event, newInput?: string) => (partitionValueRef.current = newInput)}
onParamKeyChange={(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption) =>
(partitionKeyRef.current = item.key.toString())
}
paramValue={partitionValueRef.current}
selectedKey={partitionKeyRef.current}
/>
{getInputParameterComponent()}
<Stack horizontal onClick={() => addNewParamAtLastIndex()} tabIndex={0}>
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
<Text className="addNewParamStyle">Add New Param</Text>
</Stack>
</div>
</RightPaneForm>
);

View File

@@ -55,7 +55,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<Stack horizontal>
<Dropdown
label={dropdownLabel && dropdownLabel}
selectedKey={selectedKey}
defaultSelectedKey={selectedKey}
onChange={onParamKeyChange}
options={options}
styles={dropdownStyles}
@@ -64,8 +64,9 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<TextField
label={inputLabel && inputLabel}
id="confirmCollectionId"
value={paramValue}
defaultValue={paramValue}
onChange={onParamValueChange}
tabIndex={0}
/>
{isAddRemoveVisible && (
<>

View File

@@ -35,6 +35,9 @@ interface IGitHubReposPanelState {
}
export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IGitHubReposPanelState> {
private static readonly PageSize = 30;
private static readonly MasterBranchName = "master";
private static readonly MainBranchName = "main";
private isAddedRepo = false;
private gitHubClient: GitHubClient;
private junoClient: JunoClient;
@@ -116,6 +119,8 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when saving pinned repos`);
}
this.props.explorer.notebookManager?.refreshPinnedRepos();
} catch (error) {
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
}
@@ -207,6 +212,14 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
if (response.data) {
branchesProps.branches = branchesProps.branches.concat(response.data);
branchesProps.lastPageInfo = response.pageInfo;
branchesProps.defaultBranchName = branchesProps.branches[0].name;
const defaultbranchName = branchesProps.branches.find(
(branch) =>
branch.name === GitHubReposPanel.MasterBranchName || branch.name === GitHubReposPanel.MainBranchName
)?.name;
if (defaultbranchName) {
branchesProps.defaultBranchName = defaultbranchName;
}
}
} catch (error) {
handleError(error, "GitHubReposPane/loadMoreBranches", "Failed to fetch branches");
@@ -298,6 +311,17 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
const existingRepo = this.pinnedReposProps.repos.find((repo) => repo.key === item.key);
if (existingRepo) {
existingRepo.branches = item.branches;
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
},
},
});
} else {
this.pinnedReposProps.repos = [...this.pinnedReposProps.repos, item];
}
@@ -374,6 +398,7 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
lastPageInfo: undefined,
hasMore: true,
isLoading: true,
defaultBranchName: undefined,
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
};
this.loadMoreBranches(item.repo);

View File

@@ -23,7 +23,13 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"phoenixClient": PhoenixClient {
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],

View File

@@ -3,12 +3,20 @@ import React from "react";
export interface PanelFooterProps {
buttonLabel: string;
isButtonDisabled?: boolean;
}
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ({
buttonLabel,
isButtonDisabled,
}: PanelFooterProps): JSX.Element => (
<div className="panelFooter">
<PrimaryButton type="submit" id="sidePanelOkButton" text={buttonLabel} ariaLabel={buttonLabel} />
<PrimaryButton
type="submit"
id="sidePanelOkButton"
text={buttonLabel}
ariaLabel={buttonLabel}
disabled={!!isButtonDisabled}
/>
</div>
);

View File

@@ -9,6 +9,7 @@ export interface RightPaneFormProps {
onSubmit: () => void;
submitButtonText: string;
isSubmitButtonHidden?: boolean;
isSubmitButtonDisabled?: boolean;
children?: ReactNode;
}
@@ -18,6 +19,7 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
onSubmit,
submitButtonText,
isSubmitButtonHidden = false,
isSubmitButtonDisabled = false,
children,
}: RightPaneFormProps) => {
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@@ -30,7 +32,9 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
<form className="panelFormWrapper" onSubmit={handleOnSubmit}>
{formError && <PanelInfoErrorComponent messageType="error" message={formError} showErrorDetails={true} />}
{children}
{!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />}
{!isSubmitButtonHidden && (
<PanelFooterComponent buttonLabel={submitButtonText} isButtonDisabled={isSubmitButtonDisabled} />
)}
</form>
{isExecuting && <PanelLoadingScreen />}
</>

View File

@@ -14,18 +14,21 @@ exports[`Right Pane Form should render Default properly 1`] = `
>
<PanelFooterComponent
buttonLabel="Load"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="Load"
disabled={false}
id="sidePanelOkButton"
text="Load"
type="submit"
>
<PrimaryButton
ariaLabel="Load"
disabled={false}
id="sidePanelOkButton"
text="Load"
theme={
@@ -305,6 +308,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Load"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -586,6 +590,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="Load"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -868,6 +873,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
<BaseButton
ariaLabel="Load"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -1,4 +1,4 @@
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton, Stack, Text } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext";
@@ -113,20 +113,44 @@ export const SettingsPane: FunctionComponent = () => {
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
setPageOption(option.key);
};
const choiceButtonStyles = {
flexContainer: [
{
selectors: {
".ms-ChoiceField-wrapper label": {
fontSize: 12,
paddingTop: 0,
},
".ms-ChoiceField": {
marginTop: 0,
},
},
},
],
};
return (
<RightPaneForm {...genericPaneProps}>
<div className="paneMainContent">
{shouldShowQueryPageOptions && (
<div className="settingsSection">
<div className="settingsSectionPart pageOptionsPart">
<div className="settingsSectionLabel">
Page options
<div className="settingsSectionPart">
<Stack horizontal>
<Text id="pageOptions" className="settingsSectionLabel" variant="small">
Page options
</Text>
<InfoTooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
query results per page.
</InfoTooltip>
</div>
<ChoiceGroup selectedKey={pageOption} options={pageOptionList} onChange={handleOnPageOptionChange} />
</Stack>
<ChoiceGroup
ariaLabelledBy="pageOptions"
selectedKey={pageOption}
options={pageOptionList}
styles={choiceButtonStyles}
onChange={handleOnPageOptionChange}
/>
</div>
<div className="tabs settingsSectionPart">
{isCustomPageOptionSelected() && (

View File

@@ -14,17 +14,24 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSection"
>
<div
className="settingsSectionPart pageOptionsPart"
className="settingsSectionPart"
>
<div
className="settingsSectionLabel"
<Stack
horizontal={true}
>
Page options
<Text
className="settingsSectionLabel"
id="pageOptions"
variant="small"
>
Page options
</Text>
<InfoTooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
</InfoTooltip>
</div>
</Stack>
<StyledChoiceGroup
ariaLabelledBy="pageOptions"
onChange={[Function]}
options={
Array [
@@ -39,6 +46,23 @@ exports[`Settings Pane should render Default properly 1`] = `
]
}
selectedKey="custom"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField": Object {
"marginTop": 0,
},
".ms-ChoiceField-wrapper label": Object {
"fontSize": 12,
"paddingTop": 0,
},
},
},
],
}
}
/>
</div>
<div

View File

@@ -13,7 +13,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"phoenixClient": PhoenixClient {
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -675,18 +681,21 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
</div>
<PanelFooterComponent
buttonLabel="Create"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="Create"
disabled={false}
id="sidePanelOkButton"
text="Create"
type="submit"
>
<PrimaryButton
ariaLabel="Create"
disabled={false}
id="sidePanelOkButton"
text="Create"
theme={
@@ -966,6 +975,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Create"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1247,6 +1257,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<DefaultButton
ariaLabel="Create"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1529,6 +1540,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
<BaseButton
ariaLabel="Create"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -1262,18 +1262,21 @@ exports[`Table query select Panel should render Default properly 1`] = `
</div>
<PanelFooterComponent
buttonLabel="OK"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
type="submit"
>
<PrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
theme={
@@ -1553,6 +1556,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1834,6 +1838,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -2116,6 +2121,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -33,6 +33,7 @@ const {
Inet,
Smallint,
Tinyint,
Timestamp,
} = TableConstants.CassandraType;
export const cassandraOptions = [
{ key: Text, text: Text },
@@ -50,6 +51,7 @@ export const cassandraOptions = [
{ key: Inet, text: Inet },
{ key: Smallint, text: Smallint },
{ key: Tinyint, text: Tinyint },
{ key: Timestamp, text: Timestamp },
];
export const imageProps: IImageProps = {

View File

@@ -356,18 +356,21 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
</div>
<PanelFooterComponent
buttonLabel="Add Entity"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="Add Entity"
disabled={false}
id="sidePanelOkButton"
text="Add Entity"
type="submit"
>
<PrimaryButton
ariaLabel="Add Entity"
disabled={false}
id="sidePanelOkButton"
text="Add Entity"
theme={
@@ -647,6 +650,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Add Entity"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -928,6 +932,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="Add Entity"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1210,6 +1215,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
<BaseButton
ariaLabel="Add Entity"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -357,18 +357,21 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
</div>
<PanelFooterComponent
buttonLabel="Update"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="Update"
disabled={false}
id="sidePanelOkButton"
text="Update"
type="submit"
>
<PrimaryButton
ariaLabel="Update"
disabled={false}
id="sidePanelOkButton"
text="Update"
theme={
@@ -648,6 +651,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Update"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -929,6 +933,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="Update"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1211,6 +1216,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
<BaseButton
ariaLabel="Update"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -1041,18 +1041,21 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
</div>
<PanelFooterComponent
buttonLabel="OK"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
type="submit"
>
<PrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
theme={
@@ -1332,6 +1335,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<CustomizedDefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1613,6 +1617,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<DefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1895,6 +1900,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -84,9 +84,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
const mainItems = this.createMainItems();
const commonTaskItems = this.createCommonTaskItems();
let recentItems = this.createRecentItems();
if (userContext.features.notebooksTemporarilyDown) {
recentItems = recentItems.filter((item) => item.description !== "Notebook");
}
recentItems = recentItems.filter((item) => item.description !== "Notebook");
const tipsItems = this.createTipsItems();
const onClearRecent = this.clearMostRecent;
@@ -223,7 +221,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
});
}
if (useNotebook.getState().isNotebookEnabled && !userContext.features.notebooksTemporarilyDown) {
if (useNotebook.getState().isNotebookEnabled && useNotebook.getState().isPhoenix) {
heroes.push({
iconSrc: NewNotebookIcon,
title: "New Notebook",
@@ -307,23 +305,24 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: AddDatabaseIcon,
title: "New " + getDatabaseName(),
description: undefined,
onClick: () => this.openAddDatabasePanel(),
onClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
await useDatabases.getState().loadAllOffers();
}
useSidePanel
.getState()
.openSidePanel(
"New " + getDatabaseName(),
<AddDatabasePanel explorer={this.container} buttonElement={document.activeElement as HTMLElement} />
);
},
});
}
return items;
}
private openAddDatabasePanel() {
const newDatabaseButton = document.activeElement as HTMLElement;
useSidePanel
.getState()
.openSidePanel(
"New " + getDatabaseName(),
<AddDatabasePanel explorer={this.container} buttonElement={newDatabaseButton} />
);
}
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
return {
iconSrc: NotebookIcon,

View File

@@ -19,6 +19,7 @@ export const CassandraType = {
Float: "Float",
Int: "Int",
Text: "Text",
Timestamp: "Timestamp",
Uuid: "Uuid",
Varchar: "Varchar",
Varint: "Varint",

View File

@@ -431,7 +431,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
if (newHeaders.length > 0) {
// Any new columns found will be added into headers array, which will trigger a re-render of the DataTable.
// So there is no need to call it here.
this.updateHeaders(newHeaders, /* notifyColumnChanges */ true);
this.updateHeaders(selectedHeadersUnion, /* notifyColumnChanges */ true);
} else {
if (columnSortOrder) {
this.sortColumns(columnSortOrder, oSettings);

View File

@@ -202,14 +202,21 @@ export class CassandraAPIDataClient extends TableDataClient {
let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`;
let isPropertyUpdated = false;
let isFirstPropertyToUpdate = true;
for (let property in newEntity) {
if (
!originalDocument[property] ||
newEntity[property]._.toString() !== originalDocument[property]._.toString()
) {
updateQuery += this.isStringType(newEntity[property].$)
? ` SET ${property} = '${newEntity[property]._}',`
: ` SET ${property} = ${newEntity[property]._},`;
let propertyQuerySegment = this.isStringType(newEntity[property].$)
? `${property} = '${newEntity[property]._}',`
: `${property} = ${newEntity[property]._},`;
// Only add the "SET" keyword once
if (isFirstPropertyToUpdate) {
propertyQuerySegment = " SET " + propertyQuerySegment;
isFirstPropertyToUpdate = false;
}
updateQuery += propertyQuerySegment;
isPropertyUpdated = true;
}
}
@@ -528,7 +535,8 @@ export class CassandraAPIDataClient extends TableDataClient {
dataType === TableConstants.CassandraType.Text ||
dataType === TableConstants.CassandraType.Inet ||
dataType === TableConstants.CassandraType.Ascii ||
dataType === TableConstants.CassandraType.Varchar
dataType === TableConstants.CassandraType.Varchar ||
dataType === TableConstants.CassandraType.Timestamp
);
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "./Constants";
import * as Entities from "./Entities";
@@ -17,12 +16,12 @@ enum DataTypes {
Int64 = 18,
}
const tablesIndexers = {
var tablesIndexers = {
Value: "$v",
Type: "$t",
};
export const keyProperties = {
export var keyProperties = {
PartitionKey: "$pk",
Id: "id",
Id2: "$id", // This should always be the same value as Id
@@ -34,17 +33,14 @@ export const keyProperties = {
};
export function convertDocumentsToEntities(documents: any[]): Entities.ITableEntityForTablesAPI[] {
const results: Entities.ITableEntityForTablesAPI[] = [];
let results: Entities.ITableEntityForTablesAPI[] = [];
documents &&
documents.forEach((document) => {
if (
!Object.prototype.hasOwnProperty.call(document, keyProperties.PartitionKey) ||
Object.prototype.hasOwnProperty.call(document, keyProperties.Id)
) {
if (!document.hasOwnProperty(keyProperties.PartitionKey) || !document.hasOwnProperty(keyProperties.Id)) {
//Document does not match the current required format for Tables, so we ignore it
return; // The rest of the key properties should be guaranteed as DocumentDB properties
}
const entity: Entities.ITableEntityForTablesAPI = <Entities.ITableEntityForTablesAPI>{
let entity: Entities.ITableEntityForTablesAPI = <Entities.ITableEntityForTablesAPI>{
PartitionKey: {
_: document[keyProperties.PartitionKey],
$: Constants.TableType.String,
@@ -75,8 +71,8 @@ export function convertDocumentsToEntities(documents: any[]): Entities.ITableEnt
$: Constants.TableType.String,
},
};
for (const property in document) {
if (Object.prototype.hasOwnProperty.call(document, property)) {
for (var property in document) {
if (document.hasOwnProperty(property)) {
if (
property !== keyProperties.PartitionKey &&
property !== keyProperties.Id &&
@@ -87,10 +83,7 @@ export function convertDocumentsToEntities(documents: any[]): Entities.ITableEnt
property !== keyProperties.attachments &&
property !== keyProperties.Id2
) {
if (
!Object.prototype.hasOwnProperty.call(document[property], "$v") ||
!Object.prototype.hasOwnProperty.call(document[property], "$t")
) {
if (!document[property].hasOwnProperty("$v") || !document[property].hasOwnProperty("$t")) {
return; //Document property does not match the current required format for Tables, so we ignore it
}
if (DataTypes[document[property][tablesIndexers.Type]] === DataTypes[DataTypes.DateTime]) {
@@ -118,10 +111,10 @@ export function convertEntitiesToDocuments(
entities: Entities.ITableEntityForTablesAPI[],
collection: ViewModels.Collection
): any[] {
const results: any[] = [];
let results: any[] = [];
entities &&
entities.forEach((entity) => {
const document: any = {
let document: any = {
$id: entity.RowKey._,
id: entity.RowKey._,
ts: DateTimeUtilities.convertJSDateToUnix(entity.Timestamp._), // Convert back to unix time
@@ -136,7 +129,7 @@ export function convertEntitiesToDocuments(
document[collection.partitionKeyProperty] = entity.PartitionKey._;
document["partitionKeyValue"] = entity.PartitionKey._;
}
for (const property in entity) {
for (var property in entity) {
if (
property !== Constants.EntityKeyNames.PartitionKey &&
property !== Constants.EntityKeyNames.RowKey &&
@@ -167,7 +160,7 @@ export function convertEntitiesToDocuments(
}
export function convertEntityToNewDocument(entity: Entities.ITableEntityForTablesAPI): any {
const document: any = {
let document: any = {
$pk: entity.PartitionKey._,
$id: entity.RowKey._,
id: entity.RowKey._,

View File

@@ -92,7 +92,7 @@ export default class DocumentsTab extends TabsBase {
this.partitionKeyPropertyHeader =
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
this.partitionKeyProperty = this.partitionKeyPropertyHeader
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
: null;
@@ -446,8 +446,8 @@ export default class DocumentsTab extends TabsBase {
this.partitionKey as PartitionKeyDefinition
);
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
const id = new DocumentId(this, savedDocument, partitionKeyValue);
const ids = this.documentIds();
let id = new DocumentId(this, savedDocument, partitionKeyValue);
let ids = this.documentIds();
ids.push(id);
this.selectedDocumentId(id);
@@ -682,10 +682,10 @@ export default class DocumentsTab extends TabsBase {
}
public createIterator(): QueryIterator<ItemDefinition & Resource> {
const filters = this.lastFilterContents();
let filters = this.lastFilterContents();
const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter);
const options: any = {};
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
if (this._resourceTokenPartitionKey) {
@@ -778,7 +778,7 @@ export default class DocumentsTab extends TabsBase {
protected _onEditorContentChange(newContent: string) {
try {
const parsed: any = JSON.parse(newContent);
let parsed: any = JSON.parse(newContent);
this.onValidDocumentEdit();
} catch (e) {
this.onInvalidDocumentEdit();

View File

@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { extractPartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
import * as ko from "knockout";
import Q from "q";
@@ -46,7 +44,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
super.buildCommandBarOptions();
}
public onSaveNewDocumentClick = (): Promise<void> => {
public onSaveNewDocumentClick = (): Promise<any> => {
const documentContent = JSON.parse(this.selectedDocumentContent());
this.displayedError("");
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
@@ -61,8 +59,9 @@ export default class MongoDocumentsTab extends DocumentsTab {
) {
const message = `The document is lacking the shard property: ${this.partitionKeyProperty}`;
this.displayedError(message);
let that = this;
setTimeout(() => {
this.displayedError("");
that.displayedError("");
}, Constants.ClientDefaults.errorNotificationTimeoutMs);
this.isExecutionError(true);
TelemetryProcessor.traceFailure(
@@ -83,19 +82,19 @@ export default class MongoDocumentsTab extends DocumentsTab {
return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)
.then(
(savedDocument: any) => {
const partitionKeyArray = extractPartitionKey(
let partitionKeyArray = extractPartitionKey(
savedDocument,
this._getPartitionKeyDefinition() as PartitionKeyDefinition
);
const partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
let partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
const id = new ObjectId(this, savedDocument, partitionKeyValue);
const ids = this.documentIds();
let id = new ObjectId(this, savedDocument, partitionKeyValue);
let ids = this.documentIds();
ids.push(id);
delete savedDocument._self;
const value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
let value: string = this.renderObjectForEditor(savedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value);
this.selectedDocumentId(id);
@@ -129,7 +128,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
.finally(() => this.isExecuting(false));
};
public onSaveExisitingDocumentClick = (): Promise<void> => {
public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId();
const documentContent = this.selectedDocumentContent();
this.isExecutionError(false);
@@ -142,7 +141,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent)
.then(
(updatedDocument: any) => {
const value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
this.selectedDocumentContent.setBaseline(value);
this.documentIds().forEach((documentId: DocumentId) => {
@@ -152,7 +151,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
this._getPartitionKeyDefinition() as PartitionKeyDefinition
);
const partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
let partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
const id = new ObjectId(this, updatedDocument, partitionKeyValue);
documentId.id(id.id());
@@ -197,7 +196,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
this.initDocumentEditor(documentId, content);
}
public loadNextPage(): Q.Promise<void> {
public loadNextPage(): Q.Promise<any> {
this.isExecuting(true);
this.isExecutionError(false);
const filter: string = this.filterContent().trim();
@@ -229,7 +228,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
this.selectedDocumentId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
}
if (this.onLoadStartKey !== null && this.onLoadStartKey !== undefined) {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
{
@@ -244,8 +243,8 @@ export default class MongoDocumentsTab extends DocumentsTab {
this.onLoadStartKey = null;
}
},
(error: Error) => {
if (this.onLoadStartKey !== null && this.onLoadStartKey !== undefined) {
(error: any) => {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
@@ -266,13 +265,13 @@ export default class MongoDocumentsTab extends DocumentsTab {
.finally(() => this.isExecuting(false));
}
protected _onEditorContentChange(newContent: string): void {
protected _onEditorContentChange(newContent: string) {
try {
if (
this.editorState() === ViewModels.DocumentExplorerState.newDocumentValid ||
this.editorState() === ViewModels.DocumentExplorerState.newDocumentInvalid
) {
const parsed: any = JSON.parse(newContent);
let parsed: any = JSON.parse(newContent);
}
// Mongo uses BSON format for _id, trying to parse it as JSON blocks normal flow in an edit

View File

@@ -17,6 +17,7 @@ export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
/**
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton)
* Re-initiating the constructor when ever a new container got allocated.
*/
export default class NotebookTabBase extends TabsBase {
protected static clientManager: NotebookClientV2;
@@ -27,6 +28,15 @@ export default class NotebookTabBase extends TabsBase {
this.container = options.container;
useNotebook.subscribe(
() => {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint) {
NotebookTabBase.clientManager = undefined;
}
},
(state) => state.notebookServerInfo
);
if (!NotebookTabBase.clientManager) {
NotebookTabBase.clientManager = new NotebookClientV2({
connectionInfo: useNotebook.getState().notebookServerInfo,

View File

@@ -53,14 +53,16 @@ export default class NotebookTabV2 extends NotebookTabBase {
onUpdateKernelInfo: this.onKernelUpdate,
});
}
public onCloseTabButtonClick(): Q.Promise<any> {
/*
* Hard cleaning the workspace(Closing tabs connected with old container connection) when new container got allocated.
*/
public onCloseTabButtonClick(hardClose = false): Q.Promise<any> {
const cleanup = () => {
this.notebookComponentAdapter.notebookShutdown();
super.onCloseTabButtonClick();
};
if (this.notebookComponentAdapter.isContentDirty()) {
if (this.notebookComponentAdapter.isContentDirty() && hardClose === false) {
useDialog
.getState()
.showOkCancelModalDialog(

View File

@@ -100,6 +100,7 @@ export default class TerminalTab extends TabsBase {
return {
authToken: info.authToken,
notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`,
forwardingId: info.forwardingId,
};
}
}

View File

@@ -1,4 +1,5 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
@@ -528,6 +529,9 @@ export default class Collection implements ViewModels.Collection {
};
public onSchemaAnalyzerClick = async () => {
if (useNotebook.getState().isPhoenix) {
await this.container.allocateContainer();
}
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
@@ -572,7 +576,8 @@ export default class Collection implements ViewModels.Collection {
public onSettingsClick = async (): Promise<void> => {
useSelectedNode.getState().setSelectedNode(this);
await this.loadOffer();
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Settings node",

View File

@@ -57,7 +57,7 @@ export default class Database implements ViewModels.Database {
this.isOfferRead = false;
}
public onSettingsClick = (): void => {
public onSettingsClick = async (): Promise<void> => {
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
@@ -66,6 +66,11 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.ResourceTree,
});
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
await useDatabases.getState().loadAllOffers();
}
const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification();
const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2;
const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id());

View File

@@ -121,7 +121,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
children: [],
};
if (userContext.features.notebooksTemporarilyDown) {
if (!useNotebook.getState().isPhoenix) {
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} else {
if (galleryContentRoot) {
@@ -130,9 +130,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
if (
myNotebooksContentRoot &&
((NotebookUtil.isPhoenixEnabled() &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected) ||
userContext.features.phoenix === false)
useNotebook.getState().isPhoenix &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
) {
notebooksTree.children.push(buildMyNotebooksTree());
}
@@ -166,15 +165,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
myNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (
hasOpened &&
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === false
) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
container.openNotebook(item);
}
);
@@ -189,15 +180,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (
hasOpened &&
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === false
) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
container.openNotebook(item);
},
true
);
@@ -528,7 +511,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
isNotebookEnabled &&
userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed() &&
!userContext.features.notebooksTemporarilyDown
useNotebook.getState().isPhoenix
) {
children.push({
label: "Schema (Preview)",

View File

@@ -18,6 +18,7 @@ interface DatabasesState {
findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection;
isLastCollection: () => boolean;
loadDatabaseOffers: () => Promise<void>;
loadAllOffers: () => Promise<void>;
isFirstResourceCreated: () => boolean;
findSelectedDatabase: () => ViewModels.Database;
validateDatabaseId: (id: string) => boolean;
@@ -97,6 +98,19 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
})
);
},
loadAllOffers: async () => {
await Promise.all(
get().databases?.map(async (database: ViewModels.Database) => {
await database.loadOffer();
await database.loadCollections();
await Promise.all(
(database.collections() || []).map(async (collection: ViewModels.Collection) => {
await collection.loadOffer();
})
);
})
);
},
isFirstResourceCreated: () => {
const databases = get().databases;

View File

@@ -1,3 +1,5 @@
import { ConnectionStatusType } from "Common/Constants";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
import { useTabs } from "../hooks/useTabs";
@@ -12,6 +14,7 @@ export interface SelectedNodeState {
collectionId?: string,
subnodeKinds?: ViewModels.CollectionTabKind[]
) => boolean;
isConnectedToContainer: () => boolean;
}
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
@@ -59,4 +62,7 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
subnodeKinds.includes(selectedSubnodeKind)
);
},
isConnectedToContainer: (): boolean => {
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
},
}));

View File

@@ -1,8 +1,8 @@
import ko from "knockout";
import postRobot from "post-robot";
import { GetGithubClientId } from "Utils/GitHubUtils";
import { HttpStatusCodes } from "../Common/Constants";
import { handleError } from "../Common/ErrorHandlingUtils";
import { configContext } from "../ConfigContext";
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
import { JunoClient } from "../Juno/JunoClient";
import { logConsoleInfo } from "../Utils/NotificationConsoleUtils";
@@ -55,7 +55,7 @@ export class GitHubOAuthService {
const params = {
scope,
client_id: configContext.GITHUB_CLIENT_ID,
client_id: GetGithubClientId(),
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
state: this.resetState(),
};

View File

@@ -1,4 +1,5 @@
import ko from "knockout";
import { GetGithubClientId } from "Utils/GitHubUtils";
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
@@ -522,7 +523,7 @@ export class JunoClient {
private static getGitHubClientParams(): URLSearchParams {
const githubParams = new URLSearchParams({
client_id: configContext.GITHUB_CLIENT_ID,
client_id: GetGithubClientId(),
});
if (configContext.GITHUB_CLIENT_SECRET) {

View File

@@ -1,33 +1,50 @@
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import promiseRetry, { AbortError } from "p-retry";
import { ConnectionStatusType, ContainerStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../Common/Constants";
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import { configContext } from "../ConfigContext";
import {
ContainerConnectionInfo,
ContainerInfo,
IContainerData,
IPhoenixConnectionInfoResult,
IProvisionData,
IResponse
} from "../Contracts/DataModels";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
export interface IPhoenixResponse<T> {
status: number;
data: T;
}
export interface IPhoenixConnectionInfoResult {
readonly notebookAuthToken?: string;
readonly notebookServerUrl?: string;
}
export interface IProvosionData {
cosmosEndpoint: string;
dbAccountName: string;
aadToken: string;
resourceGroup: string;
subscriptionId: string;
}
export class PhoenixClient {
public async containerConnectionInfo(
provisionData: IProvosionData
): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> {
private containerHealthHandler: NodeJS.Timeout;
private retryOptions: promiseRetry.Options = {
retries: Notebook.retryAttempts,
maxTimeout: Notebook.retryAttemptDelayMs,
minTimeout: Notebook.retryAttemptDelayMs,
};
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixConnectionInfoResult>> {
return this.executeContainerAssignmentOperation(provisionData, "allocate");
}
public async resetContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixConnectionInfoResult>> {
return this.executeContainerAssignmentOperation(provisionData, "reset");
}
private async executeContainerAssignmentOperation(
provisionData: IProvisionData,
operation: string
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
try {
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/allocate`, {
method: "POST",
headers: PhoenixClient.getHeaders(),
body: JSON.stringify(provisionData),
});
const response = await fetch(
`${this.getPhoenixControlPlaneEndpoint()}/containerconnections`,
{
method: operation === "allocate" ? "POST" : "PATCH",
headers: PhoenixClient.getHeaders(),
body: JSON.stringify(provisionData),
}
);
let data: IPhoenixConnectionInfoResult;
if (response.status === HttpStatusCodes.OK) {
data = await response.json();
@@ -42,6 +59,83 @@ export class PhoenixClient {
}
}
public async initiateContainerHeartBeat(containerData: IContainerData) {
if (this.containerHealthHandler) {
clearTimeout(this.containerHealthHandler);
}
await this.getContainerHealth(Notebook.containerStatusHeartbeatDelayMs, containerData);
}
private scheduleContainerHeartbeat(delayMs: number, containerData: IContainerData): void {
this.containerHealthHandler = setTimeout(async () => {
await this.getContainerHealth(delayMs, containerData);
}, delayMs);
}
private async getContainerStatusAsync(containerData: IContainerData): Promise<ContainerInfo> {
try {
const runContainerStatusAsync = async () => {
const response = await window.fetch(
`${this.getPhoenixControlPlaneEndpoint()}/${containerData.forwardingId}`,
{
method: "GET",
headers: PhoenixClient.getHeaders(),
}
);
if (response.status === HttpStatusCodes.OK) {
const containerStatus = await response.json();
return {
durationLeftInMinutes: containerStatus?.durationLeftInMinutes,
notebookServerInfo: containerStatus?.notebookServerInfo,
status: ContainerStatusType.Active,
};
} else if (response.status === HttpStatusCodes.NotFound) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Reconnect,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
throw new AbortError(response.statusText);
}
throw new Error(response.statusText);
};
return await promiseRetry(runContainerStatusAsync, this.retryOptions);
} catch (error) {
Logger.logError(getErrorMessage(error), "PhoenixClient/getContainerStatus");
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Reconnect,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
return {
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
status: ContainerStatusType.Disconnected,
};
}
}
private async getContainerHealth(delayMs: number, containerData: IContainerData) {
const containerInfo = await this.getContainerStatusAsync(containerData);
useNotebook.getState().setContainerStatus(containerInfo);
if (useNotebook.getState().containerStatus?.status === ContainerStatusType.Active) {
this.scheduleContainerHeartbeat(delayMs, containerData);
}
}
public async IsDbAcountWhitelisted() {
try {
const response = await window.fetch(`${this.getPhoenixControlPlaneEndpoint()}`, {
method: "GET",
headers: PhoenixClient.getHeaders(),
});
return response.status === HttpStatusCodes.OK;
} catch (error) {
Logger.logError(getErrorMessage(error), "PhoenixClient/IsDbAcountWhitelisted");
return false;
}
}
public static getPhoenixEndpoint(): string {
const phoenixEndpoint =
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
@@ -54,9 +148,10 @@ export class PhoenixClient {
return phoenixEndpoint;
}
public getPhoenixContainerPoolingEndPoint(): string {
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer`;
public getPhoenixControlPlaneEndpoint(): string {
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer/cosmosaccounts${userContext.databaseAccount.id}`;
}
private static getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader();
return {

View File

@@ -12,6 +12,7 @@ export type Features = {
partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
phoenix: boolean;
notebooksDownBanner: boolean;
readonly enableSDKoperations: boolean;
readonly enableSpark: boolean;
readonly enableTtl: boolean;
@@ -32,7 +33,7 @@ export type Features = {
readonly ttl90Days: boolean;
readonly mongoProxyEndpoint?: string;
readonly mongoProxyAPIs?: string;
readonly notebooksTemporarilyDown: boolean;
readonly enableThroughputCap: boolean;
};
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
@@ -82,8 +83,9 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
autoscaleDefault: "true" === get("autoscaledefault"),
partitionKeyDefault: "true" === get("partitionkeytest"),
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"),
phoenix: "true" === get("phoenix"),
notebooksDownBanner: "true" === get("notebooksDownBanner"),
enableThroughputCap: "true" === get("enablethroughputcap"),
};
}

View File

@@ -6,6 +6,7 @@ import { RefreshResult } from "../SelfServeTypes";
import SqlX from "./SqlX";
import {
FetchPricesResponse,
PriceMapAndCurrencyCode,
RegionsResponse,
SqlxServiceResource,
UpdateDedicatedGatewayRequestParameters,
@@ -178,18 +179,18 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => {
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
};
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promise<PriceMapAndCurrencyCode> => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "getPriceMap",
function: "getPriceMapAndCurrencyCode",
description: "fetch prices API call",
selfServeClassName: SqlX.name,
};
const getPriceMapTimestamp = selfServeTraceStart(telemetryData);
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>();
@@ -207,17 +208,21 @@ export const getPriceMap = async (regions: Array<string>): Promise<Map<string, M
});
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, getPriceMapTimestamp);
return priceMap;
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
return { priceMap: priceMap, currencyCode: currencyCode };
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getPriceMapTimestamp);
return undefined;
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
return { priceMap: undefined, currencyCode: undefined };
}
};

View File

@@ -21,7 +21,7 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils";
import {
deleteDedicatedGatewayResource,
getCurrentProvisioningState,
getPriceMap,
getPriceMapAndCurrencyCode,
getRegions,
refreshDedicatedGatewayProvisioning,
updateDedicatedGatewayResource,
@@ -207,6 +207,7 @@ const ApproximateCostDropDownInfo: Info = {
};
let priceMap: Map<string, Map<string, number>>;
let currencyCode: string;
let regions: Array<string>;
const calculateCost = (skuName: string, instanceCount: number): Description => {
@@ -237,7 +238,7 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
return {
textTKey: `${costPerHour} USD`,
textTKey: `${costPerHour} ${currencyCode}`,
type: DescriptionType.Text,
};
} catch (err) {
@@ -346,7 +347,9 @@ export default class SqlX extends SelfServeBaseClass {
});
regions = await getRegions();
priceMap = await getPriceMap(regions);
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(regions);
priceMap = priceMapAndCurrencyCode.priceMap;
currencyCode = priceMapAndCurrencyCode.currencyCode;
const response = await getCurrentProvisioningState();
if (response.status && response.status !== "Deleting") {

View File

@@ -36,9 +36,15 @@ export type FetchPricesResponse = {
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 = {

View File

@@ -50,7 +50,6 @@ export enum Action {
SubscriptionSwitch,
TenantSwitch,
DefaultTenantSwitch,
ResetNotebookWorkspace,
CreateNotebookWorkspace,
NotebookErrorNotification,
CreateSparkCluster,
@@ -82,6 +81,8 @@ export enum Action {
NotebooksInsertTextCellBelowFromMenu,
NotebooksMoveCellUpFromMenu,
NotebooksMoveCellDownFromMenu,
PhoenixConnection,
PhoenixResetWorkspace,
DeleteCellFromMenu,
OpenTerminal,
CreateMongoCollectionWithWildcardIndex,

View File

@@ -10,7 +10,6 @@ import {
SortBy,
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
@@ -229,7 +228,7 @@ export function downloadItem(
undefined,
"Download",
async () => {
if (NotebookUtil.isPhoenixEnabled()) {
if (useNotebook.getState().isPhoenix) {
await container.allocateContainer();
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
@@ -239,7 +238,7 @@ export function downloadItem(
useDialog
.getState()
.showOkModalDialog(
"Failed to Connect",
"Failed to connect",
"Failed to connect to temporary workspace. Please refresh the page and try again."
);
}

View File

@@ -1,4 +1,9 @@
// https://github.com/<owner>/<repo>/tree/<branch>
import { JunoEndpoints } from "Common/Constants";
import { configContext } from "ConfigContext";
import { userContext } from "UserContext";
// The url when users visit a repo/branch on github.com
export const RepoUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/tree\/([^?]*)/;
@@ -60,3 +65,15 @@ export function toContentUri(owner: string, repo: string, branch: string, path:
export function toRawContentUri(owner: string, repo: string, branch: string, path: string): string {
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
}
export function GetGithubClientId(): string {
const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (
junoEndpoint === JunoEndpoints.Test ||
junoEndpoint === JunoEndpoints.Test2 ||
junoEndpoint === JunoEndpoints.Test3
) {
return configContext.GITHUB_TEST_ENV_CLIENT_ID;
}
return configContext.GITHUB_CLIENT_ID;
}

View File

@@ -1,6 +1,7 @@
import useSWR from "swr";
import { configContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels";
import { userContext } from "../UserContext";
interface AccountListResult {
nextLink: string;
@@ -14,8 +15,8 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken:
headers.append("Authorization", bearer);
let accounts: Array<DatabaseAccount> = [];
let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2021-06-15`;
const apiVersion = userContext.features.enableThroughputCap ? "2021-10-15-preview" : "2021-06-15";
let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=${apiVersion}`;
while (nextLink) {
const response: Response = await fetch(nextLink, { headers });

View File

@@ -342,6 +342,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.flights.indexOf(Flights.Phoenix) !== -1) {
userContext.features.phoenix = true;
}
if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) {
userContext.features.notebooksDownBanner = true;
}
}
}

View File

@@ -1,5 +1,7 @@
import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels";
import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
import TabsBase from "../Explorer/Tabs/TabsBase";
interface TabsState {
@@ -12,6 +14,7 @@ interface TabsState {
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
closeTab: (tab: TabsBase) => void;
closeAllNotebookTabs: (hardClose: boolean) => void;
}
export const useTabs: UseStore<TabsState> = create((set, get) => ({
@@ -78,4 +81,31 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
set({ openedTabs: updatedTabs });
},
closeAllNotebookTabs: (hardClose): void => {
const isNotebook = (tabKind: CollectionTabKind): boolean => {
if (
tabKind === CollectionTabKind.Notebook ||
tabKind === CollectionTabKind.NotebookV2 ||
tabKind === CollectionTabKind.SchemaAnalyzer ||
tabKind === CollectionTabKind.Terminal
) {
return true;
}
return false;
};
const tabList = get().openedTabs;
if (tabList && tabList.length > 0) {
tabList.forEach((tab: NotebookTabV2) => {
const tabKind: CollectionTabKind = tab.tabKind;
if (tabKind && isNotebook(tabKind)) {
tab.onCloseTabButtonClick(hardClose);
}
});
if (get().openedTabs.length === 0) {
set({ activeTab: undefined });
}
}
},
}));