mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 19:01:28 +00:00
Compare commits
23 Commits
eslint/fix
...
users/dech
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f1b0b01ba | ||
|
|
8a8c023d7b | ||
|
|
667b1e1486 | ||
|
|
203c2ac246 | ||
|
|
5d235038ad | ||
|
|
6b4d6f986e | ||
|
|
e575b94ffa | ||
|
|
42bdcaf8d1 | ||
|
|
94a03e5b03 | ||
|
|
1155557af1 | ||
|
|
27a49e9aa9 | ||
|
|
fa8be2bc0f | ||
|
|
3aa4bbe266 | ||
|
|
2dfabf3c69 | ||
|
|
a3d88af175 | ||
|
|
5597a1e8b6 | ||
|
|
e3d5ad2ce8 | ||
|
|
64f36e2d28 | ||
|
|
4ce1252e58 | ||
|
|
7d9faec81e | ||
|
|
22da3b90ef | ||
|
|
361ac45e52 | ||
|
|
8aa764079a |
@@ -84,6 +84,7 @@ src/Explorer/Tables/DataTable/DataTableOperationManager.ts
|
|||||||
src/Explorer/Tables/DataTable/DataTableViewModel.ts
|
src/Explorer/Tables/DataTable/DataTableViewModel.ts
|
||||||
src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
|
src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
|
||||||
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
|
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
|
||||||
|
src/Explorer/Tables/TableDataClient.ts
|
||||||
src/Explorer/Tables/TableEntityProcessor.ts
|
src/Explorer/Tables/TableEntityProcessor.ts
|
||||||
src/Explorer/Tables/Utilities.ts
|
src/Explorer/Tables/Utilities.ts
|
||||||
src/Explorer/Tabs/ConflictsTab.ts
|
src/Explorer/Tabs/ConflictsTab.ts
|
||||||
|
|||||||
@@ -2077,7 +2077,7 @@ a:link {
|
|||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow-x: auto;
|
overflow-x: clip;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -2245,7 +2245,7 @@ a:link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.refreshColHeader {
|
.refreshColHeader {
|
||||||
padding: 3px 6px 6px 6px;
|
padding: 3px 6px 10px 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.refreshColHeader:hover {
|
.refreshColHeader:hover {
|
||||||
@@ -2869,31 +2869,31 @@ a:link {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings-pane {
|
.settingsSection {
|
||||||
.settingsSection {
|
border-bottom: 1px solid @BaseMedium;
|
||||||
border-bottom: 1px solid @BaseMedium;
|
margin-right: 24px;
|
||||||
margin-right: 24px;
|
padding: @MediumSpace 0px;
|
||||||
padding: @MediumSpace 0px;
|
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
}
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsSectionPart {
|
.settingsSectionPart {
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsSectionLabel {
|
.settingsSectionLabel {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
}
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.pageOptionsPart {
|
.pageOptionsPart {
|
||||||
padding-bottom: @MediumSpace;
|
padding-bottom: @MediumSpace;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -6326,8 +6326,8 @@
|
|||||||
},
|
},
|
||||||
"@webpack-cli/serve": {
|
"@webpack-cli/serve": {
|
||||||
"version": "1.5.2",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.2.tgz",
|
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/@webpack-cli/serve/-/serve-1.5.2.tgz",
|
||||||
"integrity": "sha512-vgJ5OLWadI8aKjDlOH3rb+dYyPd2GTZuQC/Tihjct6F9GpXGZINo3Y/IVuZVTM1eDQB+/AOsjPUWH/WySDaXvw==",
|
"integrity": "sha1-6lhLY3/2PFpHf28hYEtaIFtyyew=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@xtuc/ieee754": {
|
"@xtuc/ieee754": {
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export class Flights {
|
|||||||
public static readonly PartitionKeyTest = "partitionkeytest";
|
public static readonly PartitionKeyTest = "partitionkeytest";
|
||||||
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
||||||
public static readonly Phoenix = "phoenix";
|
public static readonly Phoenix = "phoenix";
|
||||||
|
public static readonly NotebooksDownBanner = "notebooksdownbanner";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AfecFeatures {
|
export class AfecFeatures {
|
||||||
@@ -343,7 +344,12 @@ export enum ConnectionStatusType {
|
|||||||
Connecting = "Connecting",
|
Connecting = "Connecting",
|
||||||
Connected = "Connected",
|
Connected = "Connected",
|
||||||
Failed = "Connection Failed",
|
Failed = "Connection Failed",
|
||||||
ReConnect = "Reconnect",
|
Reconnect = "Reconnect",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ContainerStatusType {
|
||||||
|
Active = "Active",
|
||||||
|
Disconnected = "Disconnected",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmulatorMasterKey =
|
export const EmulatorMasterKey =
|
||||||
@@ -356,20 +362,25 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta
|
|||||||
export class Notebook {
|
export class Notebook {
|
||||||
public static readonly defaultBasePath = "./notebooks";
|
public static readonly defaultBasePath = "./notebooks";
|
||||||
public static readonly heartbeatDelayMs = 60000;
|
public static readonly heartbeatDelayMs = 60000;
|
||||||
|
public static readonly containerStatusHeartbeatDelayMs = 30000;
|
||||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||||
public static readonly autoSaveIntervalMs = 120000;
|
public static readonly autoSaveIntervalMs = 120000;
|
||||||
public static readonly memoryGuageToGB = 1048576;
|
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 temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
|
||||||
public static readonly mongoShellTemporarilyDownMsg =
|
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.";
|
"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 =
|
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.";
|
"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 =
|
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.";
|
"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 newNotebookModalTitle = "Create notebook in temporary workspace";
|
||||||
public static newNotebookUploadModalTitle = "Upload Notebook in temporary workspace";
|
public static newNotebookUploadModalTitle = "Upload notebook to temporary workspace";
|
||||||
public static newNotebookModalContent1 =
|
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.";
|
"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 =
|
public static newNotebookModalContent2 =
|
||||||
@@ -401,3 +412,11 @@ export class TerminalQueryParams {
|
|||||||
public static readonly SubscriptionId = "subscriptionId";
|
public static readonly SubscriptionId = "subscriptionId";
|
||||||
public static readonly TerminalEndpoint = "terminalEndpoint";
|
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";
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { JunoEndpoints } from "Common/Constants";
|
||||||
|
|
||||||
export enum Platform {
|
export enum Platform {
|
||||||
Portal = "Portal",
|
Portal = "Portal",
|
||||||
Hosted = "Hosted",
|
Hosted = "Hosted",
|
||||||
@@ -23,6 +25,7 @@ export interface ConfigContext {
|
|||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
GITHUB_CLIENT_ID: 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.
|
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
@@ -52,14 +55,16 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
GRAPH_API_VERSION: "1.6",
|
GRAPH_API_VERSION: "1.6",
|
||||||
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.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
|
||||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
||||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
allowedJunoOrigins: [
|
allowedJunoOrigins: [
|
||||||
"https://juno-test.documents-dev.windows-int.net",
|
JunoEndpoints.Test,
|
||||||
"https://juno-test2.documents-dev.windows-int.net",
|
JunoEndpoints.Test2,
|
||||||
"https://tools.cosmos.azure.com",
|
JunoEndpoints.Test3,
|
||||||
"https://tools-staging.cosmos.azure.com",
|
JunoEndpoints.Prod,
|
||||||
|
JunoEndpoints.Stage,
|
||||||
"https://localhost",
|
"https://localhost",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ConnectionStatusType } from "../Common/Constants";
|
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
||||||
|
|
||||||
export interface DatabaseAccount {
|
export interface DatabaseAccount {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,6 +26,8 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
isVirtualNetworkFilterEnabled?: boolean;
|
isVirtualNetworkFilterEnabled?: boolean;
|
||||||
ipRules?: IpRule[];
|
ipRules?: IpRule[];
|
||||||
privateEndpointConnections?: unknown[];
|
privateEndpointConnections?: unknown[];
|
||||||
|
capacity?: { totalThroughputLimit: number };
|
||||||
|
locations?: DatabaseAccountResponseLocation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccountResponseLocation {
|
export interface DatabaseAccountResponseLocation {
|
||||||
@@ -426,6 +428,36 @@ export interface OperationStatus {
|
|||||||
export interface NotebookWorkspaceConnectionInfo {
|
export interface NotebookWorkspaceConnectionInfo {
|
||||||
authToken: string;
|
authToken: string;
|
||||||
notebookServerEndpoint: string;
|
notebookServerEndpoint: string;
|
||||||
|
forwardingId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerInfo {
|
||||||
|
durationLeftInMinutes: number;
|
||||||
|
notebookServerInfo: NotebookWorkspaceConnectionInfo;
|
||||||
|
status: ContainerStatusType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProvisionData {
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
dbAccountName: string;
|
||||||
|
cosmosEndpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IContainerData {
|
||||||
|
dbAccountName: string;
|
||||||
|
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 {
|
export interface NotebookWorkspaceFeedResponse {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
ProgressIndicator,
|
ProgressIndicator,
|
||||||
Text,
|
|
||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
@@ -197,7 +196,7 @@ export const Dialog: FC = () => {
|
|||||||
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{contentHtml && <Text>{contentHtml}</Text>}
|
{contentHtml}
|
||||||
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<PrimaryButton {...primaryButtonProps} />
|
<PrimaryButton {...primaryButtonProps} />
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "@fluentui/react";
|
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
|
import * as UrlUtility from "../../../Common/UrlUtility";
|
||||||
|
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
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 { RepoListItem } from "./GitHubReposComponent";
|
||||||
import { ChildrenMargin } from "./GitHubStyleConstants";
|
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 {
|
export interface AddRepoComponentProps {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
@@ -27,7 +27,6 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
|
|||||||
private static readonly ButtonText = "Add";
|
private static readonly ButtonText = "Add";
|
||||||
private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch";
|
private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch";
|
||||||
private static readonly TextFieldErrorMessage = "Invalid url";
|
private static readonly TextFieldErrorMessage = "Invalid url";
|
||||||
private static readonly DefaultBranchName = "master";
|
|
||||||
|
|
||||||
constructor(props: AddRepoComponentProps) {
|
constructor(props: AddRepoComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -78,7 +77,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
|
|||||||
});
|
});
|
||||||
let enteredUrl = this.state.textFieldValue;
|
let enteredUrl = this.state.textFieldValue;
|
||||||
if (enteredUrl.indexOf("/tree/") === -1) {
|
if (enteredUrl.indexOf("/tree/") === -1) {
|
||||||
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/${AddRepoComponent.DefaultBranchName}`);
|
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const repoInfo = GitHubUtils.fromRepoUri(enteredUrl);
|
const repoInfo = GitHubUtils.fromRepoUri(enteredUrl);
|
||||||
@@ -93,11 +92,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
|
|||||||
const item: RepoListItem = {
|
const item: RepoListItem = {
|
||||||
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
|
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
|
||||||
repo,
|
repo,
|
||||||
branches: [
|
branches: repoInfo.branch ? [{ name: repoInfo.branch }] : [],
|
||||||
{
|
|
||||||
name: repoInfo.branch,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ import { RepoListItem } from "./GitHubReposComponent";
|
|||||||
import {
|
import {
|
||||||
BranchesDropdownCheckboxStyles,
|
BranchesDropdownCheckboxStyles,
|
||||||
BranchesDropdownOptionContainerStyle,
|
BranchesDropdownOptionContainerStyle,
|
||||||
|
BranchesDropdownStyles,
|
||||||
|
BranchesDropdownWidth,
|
||||||
|
ReposListBranchesColumnWidth,
|
||||||
ReposListCheckboxStyles,
|
ReposListCheckboxStyles,
|
||||||
ReposListRepoColumnMinWidth,
|
ReposListRepoColumnMinWidth,
|
||||||
ReposListBranchesColumnWidth,
|
|
||||||
BranchesDropdownWidth,
|
|
||||||
BranchesDropdownStyles,
|
|
||||||
} from "./GitHubStyleConstants";
|
} from "./GitHubStyleConstants";
|
||||||
|
|
||||||
export interface ReposListComponentProps {
|
export interface ReposListComponentProps {
|
||||||
@@ -44,6 +44,7 @@ export interface BranchesProps {
|
|||||||
lastPageInfo?: IGitHubPageInfo;
|
lastPageInfo?: IGitHubPageInfo;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
defaultBranchName: string;
|
||||||
loadMore: () => void;
|
loadMore: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
private static readonly BranchesColumnName = "Branches";
|
private static readonly BranchesColumnName = "Branches";
|
||||||
private static readonly LoadingText = "Loading...";
|
private static readonly LoadingText = "Loading...";
|
||||||
private static readonly LoadMoreText = "Load more";
|
private static readonly LoadMoreText = "Load more";
|
||||||
private static readonly DefaultBranchName = "master";
|
private static readonly DefaultBranchNames = "master/main";
|
||||||
private static readonly FooterIndex = -1;
|
private static readonly FooterIndex = -1;
|
||||||
|
|
||||||
public render(): JSX.Element {
|
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)];
|
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) => ({
|
const options: IDropdownOption[] = branchesProps.branches.map((branch) => ({
|
||||||
key: branch.name,
|
key: branch.name,
|
||||||
text: branch.name,
|
text: branch.name,
|
||||||
@@ -198,7 +203,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
const dropdownProps: IDropdownProps = {
|
const dropdownProps: IDropdownProps = {
|
||||||
styles: BranchesDropdownStyles,
|
styles: BranchesDropdownStyles,
|
||||||
options: [],
|
options: [],
|
||||||
placeholder: ReposListComponent.DefaultBranchName,
|
placeholder: ReposListComponent.DefaultBranchNames,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -272,7 +277,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
|
|||||||
styles: ReposListCheckboxStyles,
|
styles: ReposListCheckboxStyles,
|
||||||
onChange: () => {
|
onChange: () => {
|
||||||
const repoListItem = { ...item };
|
const repoListItem = { ...item };
|
||||||
repoListItem.branches = [{ name: ReposListComponent.DefaultBranchName }];
|
repoListItem.branches = [];
|
||||||
this.props.pinRepo(repoListItem);
|
this.props.pinRepo(repoListItem);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,16 +35,19 @@ const testCassandraAccount: DataModels.DatabaseAccount = {
|
|||||||
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||||
authToken: "authToken",
|
authToken: "authToken",
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
|
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
|
||||||
|
forwardingId: "Id",
|
||||||
};
|
};
|
||||||
|
|
||||||
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||||
authToken: "authToken",
|
authToken: "authToken",
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||||
|
forwardingId: "Id",
|
||||||
};
|
};
|
||||||
|
|
||||||
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||||
authToken: "authToken",
|
authToken: "authToken",
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
||||||
|
forwardingId: "Id",
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("NotebookTerminalComponent", () => {
|
describe("NotebookTerminalComponent", () => {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class NotebookViewerComponent
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.clientManager = new NotebookClientV2({
|
this.clientManager = new NotebookClientV2({
|
||||||
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined },
|
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined, forwardingId: undefined },
|
||||||
databaseAccountName: undefined,
|
databaseAccountName: undefined,
|
||||||
defaultExperience: "NotebookViewer",
|
defaultExperience: "NotebookViewer",
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
@@ -71,6 +72,7 @@ export interface SettingsComponentState {
|
|||||||
wasAutopilotOriginallySet: boolean;
|
wasAutopilotOriginallySet: boolean;
|
||||||
isScaleSaveable: boolean;
|
isScaleSaveable: boolean;
|
||||||
isScaleDiscardable: boolean;
|
isScaleDiscardable: boolean;
|
||||||
|
throughputError: string;
|
||||||
|
|
||||||
timeToLive: TtlType;
|
timeToLive: TtlType;
|
||||||
timeToLiveBaseline: TtlType;
|
timeToLiveBaseline: TtlType;
|
||||||
@@ -124,6 +126,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private changeFeedPolicyVisible: boolean;
|
private changeFeedPolicyVisible: boolean;
|
||||||
private isFixedContainer: boolean;
|
private isFixedContainer: boolean;
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
|
private totalThroughputUsed: number;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
@@ -155,6 +158,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
wasAutopilotOriginallySet: false,
|
wasAutopilotOriginallySet: false,
|
||||||
isScaleSaveable: false,
|
isScaleSaveable: false,
|
||||||
isScaleDiscardable: false,
|
isScaleDiscardable: false,
|
||||||
|
throughputError: undefined,
|
||||||
|
|
||||||
timeToLive: undefined,
|
timeToLive: undefined,
|
||||||
timeToLiveBaseline: undefined,
|
timeToLiveBaseline: undefined,
|
||||||
@@ -208,6 +212,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
|
if (throughputCap && throughputCap !== -1) {
|
||||||
|
this.calculateTotalThroughputUsed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
@@ -254,6 +263,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.throughputError) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.state.isScaleSaveable ||
|
this.state.isScaleSaveable ||
|
||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
@@ -481,6 +494,26 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
||||||
this.setState({ isMongoIndexingPolicyDiscardable });
|
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 => {
|
public getAnalyticalStorageTtl = (): number => {
|
||||||
if (this.isAnalyticalStorageEnabled) {
|
if (this.isAnalyticalStorageEnabled) {
|
||||||
if (this.state.analyticalStorageTtlSelection === TtlType.On) {
|
if (this.state.analyticalStorageTtlSelection === TtlType.On) {
|
||||||
@@ -643,10 +676,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
return buttons;
|
return buttons;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMaxAutoPilotThroughputChange = (newThroughput: number): void =>
|
private onMaxAutoPilotThroughputChange = (newThroughput: number): void => {
|
||||||
this.setState({ autoPilotThroughput: newThroughput });
|
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 =>
|
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||||
@@ -893,6 +947,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onScaleSaveableChange: this.onScaleSaveableChange,
|
onScaleSaveableChange: this.onScaleSaveableChange,
|
||||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||||
initialNotification: this.props.settingsTab.pendingNotification(),
|
initialNotification: this.props.settingsTab.pendingNotification(),
|
||||||
|
throughputError: this.state.throughputError,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.isCollectionSettingsTab) {
|
if (!this.isCollectionSettingsTab) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface ScaleComponentProps {
|
|||||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
initialNotification: DataModels.Notification;
|
initialNotification: DataModels.Notification;
|
||||||
|
throughputError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||||
@@ -189,6 +190,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||||
usageSizeInKB={this.props.collection?.usageSizeInKB()}
|
usageSizeInKB={this.props.collection?.usageSizeInKB()}
|
||||||
|
throughputError={this.props.throughputError}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||||
getThroughputWarningMessage: () => JSX.Element;
|
getThroughputWarningMessage: () => JSX.Element;
|
||||||
usageSizeInKB: number;
|
usageSizeInKB: number;
|
||||||
|
throughputError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThroughputInputAutoPilotV3State {
|
interface ThroughputInputAutoPilotV3State {
|
||||||
@@ -540,6 +541,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||||
onChange={this.onAutoPilotThroughputChange}
|
onChange={this.onAutoPilotThroughputChange}
|
||||||
min={minAutoPilotThroughput}
|
min={minAutoPilotThroughput}
|
||||||
|
errorMessage={this.props.throughputError}
|
||||||
/>
|
/>
|
||||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||||
{this.minRUperGBSurvey()}
|
{this.minRUperGBSurvey()}
|
||||||
@@ -579,6 +581,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
}
|
}
|
||||||
onChange={this.onThroughputChange}
|
onChange={this.onThroughputChange}
|
||||||
min={this.props.minimum}
|
min={this.props.minimum}
|
||||||
|
errorMessage={this.props.throughputError}
|
||||||
/>
|
/>
|
||||||
{this.state.exceedFreeTierThroughput && (
|
{this.state.exceedFreeTierThroughput && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
|
|||||||
@@ -34,7 +34,13 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {},
|
"phoenixClient": PhoenixClient {
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -102,7 +108,13 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {},
|
"phoenixClient": PhoenixClient {
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const props = {
|
|||||||
isSharded: true,
|
isSharded: true,
|
||||||
setThroughputValue: () => jest.fn(),
|
setThroughputValue: () => jest.fn(),
|
||||||
setIsAutoscale: () => jest.fn(),
|
setIsAutoscale: () => jest.fn(),
|
||||||
|
setIsThroughputCapExceeded: () => jest.fn(),
|
||||||
onCostAcknowledgeChange: () => jest.fn(),
|
onCostAcknowledgeChange: () => jest.fn(),
|
||||||
};
|
};
|
||||||
describe("ThroughputInput Pane", () => {
|
describe("ThroughputInput Pane", () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
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 * as Constants from "../../../Common/Constants";
|
||||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
import * as SharedConstants from "../../../Shared/Constants";
|
||||||
@@ -16,6 +17,7 @@ export interface ThroughputInputProps {
|
|||||||
showFreeTierExceedThroughputTooltip: boolean;
|
showFreeTierExceedThroughputTooltip: boolean;
|
||||||
setThroughputValue: (throughput: number) => void;
|
setThroughputValue: (throughput: number) => void;
|
||||||
setIsAutoscale: (isAutoscale: boolean) => void;
|
setIsAutoscale: (isAutoscale: boolean) => void;
|
||||||
|
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
|
||||||
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
|
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +26,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
showFreeTierExceedThroughputTooltip,
|
showFreeTierExceedThroughputTooltip,
|
||||||
setThroughputValue,
|
setThroughputValue,
|
||||||
setIsAutoscale,
|
setIsAutoscale,
|
||||||
|
setIsThroughputCapExceeded,
|
||||||
isSharded,
|
isSharded,
|
||||||
onCostAcknowledgeChange,
|
onCostAcknowledgeChange,
|
||||||
}: ThroughputInputProps) => {
|
}: ThroughputInputProps) => {
|
||||||
@@ -31,10 +34,60 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
|
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
|
||||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||||
const [throughputError, setThroughputError] = useState<string>("");
|
const [throughputError, setThroughputError] = useState<string>("");
|
||||||
|
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||||
|
|
||||||
setIsAutoscale(isAutoscaleSelected);
|
setIsAutoscale(isAutoscaleSelected);
|
||||||
setThroughputValue(throughput);
|
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 => {
|
const getThroughputLabelText = (): string => {
|
||||||
let throughputHeaderText: string;
|
let throughputHeaderText: string;
|
||||||
if (isAutoscaleSelected) {
|
if (isAutoscaleSelected) {
|
||||||
@@ -60,11 +113,17 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
const newThroughput = parseInt(newInput);
|
const newThroughput = parseInt(newInput);
|
||||||
setThroughput(newThroughput);
|
setThroughput(newThroughput);
|
||||||
setThroughputValue(newThroughput);
|
setThroughputValue(newThroughput);
|
||||||
|
|
||||||
if (!isSharded && newThroughput > 10000) {
|
if (!isSharded && newThroughput > 10000) {
|
||||||
setThroughputError("Unsharded collections support up to 10,000 RUs");
|
setThroughputError("Unsharded collections support up to 10,000 RUs");
|
||||||
} else {
|
return;
|
||||||
setThroughputError("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!checkThroughputCap(newThroughput)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setThroughputError("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAutoScaleTooltip = (): string => {
|
const getAutoScaleTooltip = (): string => {
|
||||||
@@ -96,11 +155,13 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
setIsAutoScaleSelected(true);
|
setIsAutoScaleSelected(true);
|
||||||
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
||||||
setIsAutoscale(true);
|
setIsAutoscale(true);
|
||||||
|
checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput);
|
||||||
} else {
|
} else {
|
||||||
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||||
setIsAutoScaleSelected(false);
|
setIsAutoScaleSelected(false);
|
||||||
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||||
setIsAutoscale(false);
|
setIsAutoscale(false);
|
||||||
|
checkThroughputCap(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
isSharded={true}
|
isSharded={true}
|
||||||
onCostAcknowledgeChange={[Function]}
|
onCostAcknowledgeChange={[Function]}
|
||||||
setIsAutoscale={[Function]}
|
setIsAutoscale={[Function]}
|
||||||
|
setIsThroughputCapExceeded={[Function]}
|
||||||
setThroughputValue={[Function]}
|
setThroughputValue={[Function]}
|
||||||
showFreeTierExceedThroughputTooltip={true}
|
showFreeTierExceedThroughputTooltip={true}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import { Link } from "@fluentui/react/lib/Link";
|
import { Link } from "@fluentui/react/lib/Link";
|
||||||
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
|
import shallow from "zustand/shallow";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||||
import * as Constants from "../Common/Constants";
|
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 { readCollection } from "../Common/dataAccess/readCollection";
|
||||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||||
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
|
|
||||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { QueriesClient } from "../Common/QueriesClient";
|
import { QueriesClient } from "../Common/QueriesClient";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { ContainerConnectionInfo } from "../Contracts/DataModels";
|
import { ContainerConnectionInfo, IPhoenixConnectionInfoResult, IResponse } from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
@@ -165,11 +166,9 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useNotebook.subscribe(
|
useNotebook.subscribe(
|
||||||
async () => {
|
async () => this.initiateAndRefreshNotebookList(),
|
||||||
this.initiateAndRefreshNotebookList();
|
(state) => [state.isNotebookEnabled, state.isRefreshed],
|
||||||
useNotebook.getState().setIsRefreshed(false);
|
shallow
|
||||||
},
|
|
||||||
(state) => state.isNotebookEnabled || state.isRefreshed
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.resourceTree = new ResourceTreeAdapter(this);
|
this.resourceTree = new ResourceTreeAdapter(this);
|
||||||
@@ -179,6 +178,7 @@ export default class Explorer {
|
|||||||
useNotebook.getState().setNotebookServerInfo({
|
useNotebook.getState().setNotebookServerInfo({
|
||||||
notebookServerEndpoint: userContext.features.notebookServerUrl,
|
notebookServerEndpoint: userContext.features.notebookServerUrl,
|
||||||
authToken: userContext.features.notebookServerToken,
|
authToken: userContext.features.notebookServerToken,
|
||||||
|
forwardingId: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +364,7 @@ export default class Explorer {
|
|||||||
useNotebook.getState().setNotebookServerInfo({
|
useNotebook.getState().setNotebookServerInfo({
|
||||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
||||||
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
||||||
|
forwardingId: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,9 +376,12 @@ export default class Explorer {
|
|||||||
public async allocateContainer(): Promise<void> {
|
public async allocateContainer(): Promise<void> {
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
const isAllocating = useNotebook.getState().isAllocating;
|
const isAllocating = useNotebook.getState().isAllocating;
|
||||||
if (isAllocating === false && notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined) {
|
if (
|
||||||
|
isAllocating === false &&
|
||||||
|
(notebookServerInfo === undefined ||
|
||||||
|
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
|
||||||
|
) {
|
||||||
const provisionData = {
|
const provisionData = {
|
||||||
aadToken: userContext.authorizationToken,
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
subscriptionId: userContext.subscriptionId,
|
||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
dbAccountName: userContext.databaseAccount.name,
|
dbAccountName: userContext.databaseAccount.name,
|
||||||
@@ -388,38 +392,61 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
try {
|
try {
|
||||||
|
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
useNotebook.getState().setIsAllocating(true);
|
useNotebook.getState().setIsAllocating(true);
|
||||||
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
|
const connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||||
if (
|
if (connectionInfo.status !== HttpStatusCodes.OK) {
|
||||||
connectionInfo.status === HttpStatusCodes.OK &&
|
throw new Error(`Received status code: ${connectionInfo?.status}`);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
if (!connectionInfo?.data?.notebookServerUrl) {
|
||||||
|
throw new Error(`NotebookServerUrl is invalid!`);
|
||||||
|
}
|
||||||
|
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||||
|
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
TelemetryProcessor.traceFailure(Action.PhoenixConnection, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
});
|
||||||
connectionStatus.status = ConnectionStatusType.Failed;
|
connectionStatus.status = ConnectionStatusType.Failed;
|
||||||
useNotebook.getState().resetConatinerConnection(connectionStatus);
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
throw error;
|
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 {
|
public resetNotebookWorkspace(): void {
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
||||||
handleError(
|
handleError(
|
||||||
@@ -428,11 +455,14 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dialogContent = NotebookUtil.isPhoenixEnabled()
|
||||||
|
? "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 = {
|
const resetConfirmationDialogProps: DialogProps = {
|
||||||
isModal: true,
|
isModal: true,
|
||||||
title: "Reset Workspace",
|
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",
|
primaryButtonText: "OK",
|
||||||
secondaryButtonText: "Cancel",
|
secondaryButtonText: "Cancel",
|
||||||
onPrimaryButtonClick: this._resetNotebookWorkspace,
|
onPrimaryButtonClick: this._resetNotebookWorkspace,
|
||||||
@@ -490,16 +520,54 @@ export default class Explorer {
|
|||||||
private _resetNotebookWorkspace = async () => {
|
private _resetNotebookWorkspace = async () => {
|
||||||
useDialog.getState().closeDialog();
|
useDialog.getState().closeDialog();
|
||||||
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
||||||
|
let connectionStatus: ContainerConnectionInfo;
|
||||||
try {
|
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 (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
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 (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
}
|
||||||
logConsoleInfo("Successfully reset notebook workspace");
|
logConsoleInfo("Successfully reset notebook workspace");
|
||||||
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
|
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleError(`Failed to reset notebook workspace: ${error}`);
|
logConsoleError(`Failed to reset notebook workspace: ${error}`);
|
||||||
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
|
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
errorStack: getErrorStack(error),
|
errorStack: getErrorStack(error),
|
||||||
});
|
});
|
||||||
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
connectionStatus = {
|
||||||
|
status: ConnectionStatusType.Failed,
|
||||||
|
};
|
||||||
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearInProgressMessage();
|
clearInProgressMessage();
|
||||||
@@ -692,7 +760,7 @@ export default class Explorer {
|
|||||||
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
||||||
}
|
}
|
||||||
if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) {
|
if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) {
|
||||||
this.allocateContainer();
|
await this.allocateContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
const notebookTabs = useTabs
|
const notebookTabs = useTabs
|
||||||
@@ -1016,8 +1084,8 @@ export default class Explorer {
|
|||||||
useDialog
|
useDialog
|
||||||
.getState()
|
.getState()
|
||||||
.showOkModalDialog(
|
.showOkModalDialog(
|
||||||
"Failed to Connect",
|
"Failed to connect",
|
||||||
"Failed to connect temporary workspace, this could happen because of network issue please refresh and try again."
|
"Failed to connect to temporary workspace. This could happen because of network issues. Please refresh the page and try again."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1119,7 +1187,10 @@ export default class Explorer {
|
|||||||
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
|
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await useDatabases.getState().loadDatabaseOffers();
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
|
throughputCap && throughputCap !== -1
|
||||||
|
? await useDatabases.getState().loadAllOffers()
|
||||||
|
: await useDatabases.getState().loadDatabaseOffers();
|
||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
|
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
|
||||||
@@ -1145,10 +1216,9 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handleOpenFileAction(path: string): Promise<void> {
|
public async handleOpenFileAction(path: string): Promise<void> {
|
||||||
if (
|
if (userContext.features.phoenix) {
|
||||||
userContext.features.phoenix === false &&
|
await this.allocateContainer();
|
||||||
!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))
|
} else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
|
||||||
) {
|
|
||||||
this._openSetupNotebooksPaneForQuickstart();
|
this._openSetupNotebooksPaneForQuickstart();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1232,15 +1302,11 @@ export default class Explorer {
|
|||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: this.refreshAllDatabases();
|
||||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||||
let isNotebookEnabled = true;
|
|
||||||
if (!userContext.features.phoenix) {
|
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||||
isNotebookEnabled =
|
const isNotebookEnabled = userContext.features.notebooksDownBanner || userContext.features.phoenix;
|
||||||
userContext.authType !== AuthType.ResourceToken &&
|
|
||||||
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
|
|
||||||
userContext.features.enableNotebooks);
|
|
||||||
}
|
|
||||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||||
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
|
useNotebook.getState().setIsShellEnabled(userContext.features.phoenix && isPublicInternetAccessAllowed());
|
||||||
|
|
||||||
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
|
||||||
isNotebookEnabled,
|
isNotebookEnabled,
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
notebookButtons.push(createOpenTerminalButton(container));
|
notebookButtons.push(createOpenTerminalButton(container));
|
||||||
if (userContext.features.phoenix === false) {
|
if (selectedNodeState.isConnectedToContainer()) {
|
||||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -310,8 +310,13 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
|||||||
return {
|
return {
|
||||||
iconSrc: AddDatabaseIcon,
|
iconSrc: AddDatabaseIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () =>
|
onCommandClick: async () => {
|
||||||
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />),
|
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,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
|
import {
|
||||||
import { ActionButton } from "@fluentui/react/lib/Button";
|
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 * as React from "react";
|
||||||
import "../../../../less/hostedexplorer.less";
|
import "../../../../less/hostedexplorer.less";
|
||||||
import { ConnectionStatusType, Notebook } from "../../../Common/Constants";
|
import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import "../CommandBar/ConnectionStatusComponent.less";
|
import "../CommandBar/ConnectionStatusComponent.less";
|
||||||
@@ -10,12 +22,33 @@ interface Props {
|
|||||||
container: Explorer;
|
container: Explorer;
|
||||||
}
|
}
|
||||||
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
|
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
|
||||||
|
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
const [second, setSecond] = React.useState("00");
|
const [second, setSecond] = React.useState("00");
|
||||||
const [minute, setMinute] = React.useState("00");
|
const [minute, setMinute] = React.useState("00");
|
||||||
const [isActive, setIsActive] = React.useState(false);
|
const [isActive, setIsActive] = React.useState(false);
|
||||||
const [counter, setCounter] = React.useState(0);
|
const [counter, setCounter] = React.useState(0);
|
||||||
const [statusColor, setStatusColor] = React.useState("");
|
const [statusColor, setStatusColor] = React.useState("");
|
||||||
const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
|
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(() => {
|
React.useEffect(() => {
|
||||||
let intervalId: NodeJS.Timeout;
|
let intervalId: NodeJS.Timeout;
|
||||||
|
|
||||||
@@ -35,6 +68,15 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
|||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [isActive, counter]);
|
}, [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 = () => {
|
const stopTimer = () => {
|
||||||
setIsActive(false);
|
setIsActive(false);
|
||||||
setCounter(0);
|
setCounter(0);
|
||||||
@@ -42,15 +84,13 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
|||||||
setMinute("00");
|
setMinute("00");
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
|
||||||
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
|
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
|
||||||
|
|
||||||
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
|
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
|
||||||
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
|
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
connectionInfo &&
|
connectionInfo &&
|
||||||
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.ReConnect)
|
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
|
<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) {
|
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
|
||||||
|
stopTimer();
|
||||||
setIsActive(true);
|
setIsActive(true);
|
||||||
setStatusColor("status connecting is-animating");
|
setStatusColor("status connecting is-animating");
|
||||||
setToolTipContent("Connecting to temporary workspace.");
|
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.");
|
setToolTipContent("Click here to Reconnect to temporary workspace.");
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ActionButton
|
<>
|
||||||
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
|
<TooltipHost
|
||||||
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
|
content={
|
||||||
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
|
containerInfo?.status === ContainerStatusType.Active
|
||||||
}
|
? `Connected to temporary workspace. This temporary workspace will get disconnected in ${Math.round(
|
||||||
>
|
containerInfo.durationLeftInMinutes
|
||||||
<TooltipHost content={toolTipContent}>
|
)} minutes.`
|
||||||
<Stack className="connectionStatusContainer" horizontal>
|
: toolTipContent
|
||||||
<i className={statusColor}></i>
|
}
|
||||||
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
|
>
|
||||||
{connectionInfo.status}
|
<ActionButton
|
||||||
</span>
|
id={buttonId}
|
||||||
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
|
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
|
||||||
<ProgressIndicator description={minute + ":" + second} />
|
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
|
||||||
)}
|
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
|
||||||
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
|
}
|
||||||
<ProgressIndicator
|
>
|
||||||
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
<Stack className="connectionStatusContainer" horizontal>
|
||||||
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
<i className={statusColor}></i>
|
||||||
percentComplete={usedGB / totalGB}
|
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
|
||||||
/>
|
{connectionInfo.status}
|
||||||
)}
|
</span>
|
||||||
</Stack>
|
{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>
|
</TooltipHost>
|
||||||
</ActionButton>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* Notebook container related stuff
|
* Notebook container related stuff
|
||||||
*/
|
*/
|
||||||
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
import * as Constants from "../../Common/Constants";
|
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 { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
|
import {
|
||||||
|
ContainerConnectionInfo,
|
||||||
|
IPhoenixConnectionInfoResult,
|
||||||
|
IProvisionData,
|
||||||
|
IResponse,
|
||||||
|
} from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||||
|
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { NotebookUtil } from "./NotebookUtil";
|
import { NotebookUtil } from "./NotebookUtil";
|
||||||
import { useNotebook } from "./useNotebook";
|
import { useNotebook } from "./useNotebook";
|
||||||
@@ -16,8 +24,16 @@ import { useNotebook } from "./useNotebook";
|
|||||||
export class NotebookContainerClient {
|
export class NotebookContainerClient {
|
||||||
private clearReconnectionAttemptMessage? = () => {};
|
private clearReconnectionAttemptMessage? = () => {};
|
||||||
private isResettingWorkspace: boolean;
|
private isResettingWorkspace: boolean;
|
||||||
|
private phoenixClient: PhoenixClient;
|
||||||
|
private retryOptions: promiseRetry.Options;
|
||||||
|
|
||||||
constructor(private onConnectionLost: () => void) {
|
constructor(private onConnectionLost: () => void) {
|
||||||
|
this.phoenixClient = new PhoenixClient();
|
||||||
|
this.retryOptions = {
|
||||||
|
retries: Notebook.retryAttempts,
|
||||||
|
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||||
|
minTimeout: Notebook.retryAttemptDelayMs,
|
||||||
|
};
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||||
@@ -38,10 +54,23 @@ export class NotebookContainerClient {
|
|||||||
* Heartbeat: each ping schedules another ping
|
* Heartbeat: each ping schedules another ping
|
||||||
*/
|
*/
|
||||||
private scheduleHeartbeat(delayMs: number): void {
|
private scheduleHeartbeat(delayMs: number): void {
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
this.getMemoryUsage()
|
try {
|
||||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
|
const memoryUsageInfo = await this.getMemoryUsage();
|
||||||
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
|
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
|
||||||
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||||
|
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||||
|
}
|
||||||
|
} catch (exception) {
|
||||||
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
const connectionStatus: ContainerConnectionInfo = {
|
||||||
|
status: ConnectionStatusType.Failed,
|
||||||
|
};
|
||||||
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
}
|
||||||
|
}
|
||||||
}, delayMs);
|
}, delayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +88,27 @@ export class NotebookContainerClient {
|
|||||||
|
|
||||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
||||||
try {
|
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.checkStatus()) {
|
||||||
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
|
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -78,44 +128,43 @@ export class NotebookContainerClient {
|
|||||||
freeKB: memoryUsageInfo.free,
|
freeKB: memoryUsageInfo.free,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (NotebookUtil.isPhoenixEnabled()) {
|
} else if (response.status === HttpStatusCodes.NotFound) {
|
||||||
const connectionStatus: ContainerConnectionInfo = {
|
throw new AbortError(response.statusText);
|
||||||
status: ConnectionStatusType.ReConnect,
|
|
||||||
};
|
|
||||||
useNotebook.getState().resetConatinerConnection(connectionStatus);
|
|
||||||
useNotebook.getState().setIsRefreshed(true);
|
|
||||||
}
|
}
|
||||||
return undefined;
|
throw new Error();
|
||||||
} catch (error) {
|
} else {
|
||||||
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();
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resetWorkspace(): Promise<void> {
|
private checkStatus(): boolean {
|
||||||
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
if (useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Disconnected) {
|
||||||
|
const connectionStatus: ContainerConnectionInfo = {
|
||||||
|
status: ConnectionStatusType.Reconnect,
|
||||||
|
};
|
||||||
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||||
this.isResettingWorkspace = true;
|
this.isResettingWorkspace = true;
|
||||||
|
let response: IResponse<IPhoenixConnectionInfoResult>;
|
||||||
try {
|
try {
|
||||||
await this._resetWorkspace();
|
response = await this._resetWorkspace();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Promise.reject(error);
|
Promise.reject(error);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
this.isResettingWorkspace = false;
|
this.isResettingWorkspace = false;
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _resetWorkspace(): Promise<void> {
|
private async _resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||||
const error = "No server endpoint detected";
|
const error = "No server endpoint detected";
|
||||||
@@ -123,15 +172,23 @@ export class NotebookContainerClient {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
|
||||||
try {
|
try {
|
||||||
await fetch(`${notebookServerEndpoint}/api/shutdown`, {
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
method: "POST",
|
const provisionData: IProvisionData = {
|
||||||
headers: { Authorization: authToken },
|
subscriptionId: userContext.subscriptionId,
|
||||||
});
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
dbAccountName: userContext.databaseAccount.name,
|
||||||
|
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||||
|
};
|
||||||
|
return await this.phoenixClient.resetContainer(provisionData);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
|
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
|
||||||
await this.recreateNotebookWorkspaceAsync();
|
if (!NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
await this.recreateNotebookWorkspaceAsync();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,4 +220,12 @@ export class NotebookContainerClient {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getHeaders(): HeadersInit {
|
||||||
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
|
return {
|
||||||
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
|
[HttpHeaders.contentType]: "application/json",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe("auto start kernel", () => {
|
|||||||
connectionInfo: {
|
connectionInfo: {
|
||||||
authToken: "autToken",
|
authToken: "autToken",
|
||||||
notebookServerEndpoint: "notebookServerEndpoint",
|
notebookServerEndpoint: "notebookServerEndpoint",
|
||||||
|
forwardingId: "Id",
|
||||||
},
|
},
|
||||||
databaseAccountName: undefined,
|
databaseAccountName: undefined,
|
||||||
defaultExperience: undefined,
|
defaultExperience: undefined,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
|||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
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 { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -35,6 +36,7 @@ interface NotebookState {
|
|||||||
notebookFolderName: string;
|
notebookFolderName: string;
|
||||||
isAllocating: boolean;
|
isAllocating: boolean;
|
||||||
isRefreshed: boolean;
|
isRefreshed: boolean;
|
||||||
|
containerStatus: ContainerInfo;
|
||||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||||
@@ -53,8 +55,9 @@ interface NotebookState {
|
|||||||
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
|
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
|
||||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
|
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
|
||||||
setIsAllocating: (isAllocating: boolean) => void;
|
setIsAllocating: (isAllocating: boolean) => void;
|
||||||
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo) => void;
|
resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void;
|
||||||
setIsRefreshed: (isAllocating: boolean) => void;
|
setIsRefreshed: (isAllocating: boolean) => void;
|
||||||
|
setContainerStatus: (containerStatus: ContainerInfo) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||||
@@ -63,6 +66,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
notebookServerInfo: {
|
notebookServerInfo: {
|
||||||
notebookServerEndpoint: undefined,
|
notebookServerEndpoint: undefined,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
|
forwardingId: undefined,
|
||||||
},
|
},
|
||||||
sparkClusterConnectionInfo: {
|
sparkClusterConnectionInfo: {
|
||||||
userName: undefined,
|
userName: undefined,
|
||||||
@@ -83,6 +87,11 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
notebookFolderName: undefined,
|
notebookFolderName: undefined,
|
||||||
isAllocating: false,
|
isAllocating: false,
|
||||||
isRefreshed: false,
|
isRefreshed: false,
|
||||||
|
containerStatus: {
|
||||||
|
status: undefined,
|
||||||
|
durationLeftInMinutes: undefined,
|
||||||
|
notebookServerInfo: undefined,
|
||||||
|
},
|
||||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||||
@@ -270,13 +279,17 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
},
|
},
|
||||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
|
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
|
||||||
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
|
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
|
||||||
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo): void => {
|
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
|
||||||
|
useTabs.getState().closeAllNotebookTabs(true);
|
||||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
useNotebook.getState().setNotebookServerInfo({
|
useNotebook.getState().setNotebookServerInfo(undefined);
|
||||||
notebookServerEndpoint: undefined,
|
|
||||||
authToken: undefined,
|
|
||||||
});
|
|
||||||
useNotebook.getState().setIsAllocating(false);
|
useNotebook.getState().setIsAllocating(false);
|
||||||
|
useNotebook.getState().setContainerStatus({
|
||||||
|
status: undefined,
|
||||||
|
durationLeftInMinutes: undefined,
|
||||||
|
notebookServerInfo: undefined,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
||||||
|
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Separator,
|
Separator,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TooltipHost,
|
TooltipHost
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { createCollection } from "Common/dataAccess/createCollection";
|
import { createCollection } from "Common/dataAccess/createCollection";
|
||||||
@@ -92,6 +92,7 @@ export interface AddCollectionPanelState {
|
|||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
showErrorDetails: boolean;
|
showErrorDetails: boolean;
|
||||||
isExecuting: boolean;
|
isExecuting: boolean;
|
||||||
|
isThroughputCapExceeded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||||
@@ -122,6 +123,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
showErrorDetails: false,
|
showErrorDetails: false,
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
|
isThroughputCapExceeded: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +251,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||||
|
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||||
|
this.setState({ isThroughputCapExceeded })
|
||||||
|
}
|
||||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -463,8 +468,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
|
content={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
|
||||||
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
|
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
|
||||||
true
|
true
|
||||||
).toLocaleLowerCase()} in the database and
|
).toLocaleLowerCase()} in the database and
|
||||||
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||||
>
|
>
|
||||||
@@ -480,6 +485,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||||
|
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||||
|
this.setState({ isThroughputCapExceeded })
|
||||||
|
}
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
||||||
this.isCostAcknowledged = isAcknowledged;
|
this.isCostAcknowledged = isAcknowledged;
|
||||||
}}
|
}}
|
||||||
@@ -659,7 +667,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="My partition key is larger than 100 bytes"
|
label="My partition key is larger than 101 bytes"
|
||||||
checked={this.state.useHashV2}
|
checked={this.state.useHashV2}
|
||||||
styles={{
|
styles={{
|
||||||
text: { fontSize: 12 },
|
text: { fontSize: 12 },
|
||||||
@@ -676,7 +684,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PanelFooterComponent buttonLabel="OK" />
|
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
|
||||||
|
|
||||||
{this.state.isExecuting && <PanelLoadingScreen />}
|
{this.state.isExecuting && <PanelLoadingScreen />}
|
||||||
</form>
|
</form>
|
||||||
@@ -1011,10 +1019,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
const partitionKeyVersion = this.state.useHashV2 ? 2 : undefined;
|
const partitionKeyVersion = this.state.useHashV2 ? 2 : undefined;
|
||||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||||
? {
|
? {
|
||||||
paths: [partitionKeyString],
|
paths: [partitionKeyString],
|
||||||
kind: "Hash",
|
kind: "Hash",
|
||||||
version: partitionKeyVersion,
|
version: partitionKeyVersion,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
|
|||||||
|
|
||||||
export interface AddDatabasePaneProps {
|
export interface AddDatabasePaneProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
|
buttonElement?: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||||
explorer: container,
|
explorer: container,
|
||||||
|
buttonElement,
|
||||||
}: AddDatabasePaneProps) => {
|
}: AddDatabasePaneProps) => {
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
let throughput: number;
|
let throughput: number;
|
||||||
@@ -50,6 +52,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
);
|
);
|
||||||
const [formErrors, setFormErrors] = useState<string>("");
|
const [formErrors, setFormErrors] = useState<string>("");
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
|
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false);
|
||||||
|
|
||||||
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
|
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
|
||||||
|
|
||||||
@@ -77,6 +80,9 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
};
|
};
|
||||||
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
|
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
|
||||||
|
if (buttonElement) {
|
||||||
|
buttonElement.focus();
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
@@ -166,6 +172,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
formError: formErrors,
|
formError: formErrors,
|
||||||
isExecuting,
|
isExecuting,
|
||||||
submitButtonText: "OK",
|
submitButtonText: "OK",
|
||||||
|
isSubmitButtonDisabled: isThroughputCapExceeded,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,6 +243,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
isSharded={databaseCreateNewShared}
|
isSharded={databaseCreateNewShared}
|
||||||
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
|
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
|
||||||
|
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
<RightPaneForm
|
<RightPaneForm
|
||||||
formError=""
|
formError=""
|
||||||
isExecuting={false}
|
isExecuting={false}
|
||||||
|
isSubmitButtonDisabled={false}
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
submitButtonText="OK"
|
submitButtonText="OK"
|
||||||
>
|
>
|
||||||
@@ -92,6 +93,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
|||||||
isSharded={true}
|
isSharded={true}
|
||||||
onCostAcknowledgeChange={[Function]}
|
onCostAcknowledgeChange={[Function]}
|
||||||
setIsAutoscale={[Function]}
|
setIsAutoscale={[Function]}
|
||||||
|
setIsThroughputCapExceeded={[Function]}
|
||||||
setThroughputValue={[Function]}
|
setThroughputValue={[Function]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false);
|
const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false);
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||||
const [formError, setFormError] = useState<string>("");
|
const [formError, setFormError] = useState<string>("");
|
||||||
|
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false);
|
||||||
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
|
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
|
||||||
|
|
||||||
const addCollectionPaneOpenMessage = {
|
const addCollectionPaneOpenMessage = {
|
||||||
@@ -149,6 +150,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
formError,
|
formError,
|
||||||
isExecuting,
|
isExecuting,
|
||||||
submitButtonText: "OK",
|
submitButtonText: "OK",
|
||||||
|
isSubmitButtonDisabled: isThroughputCapExceeded,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -262,6 +264,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
isSharded
|
isSharded
|
||||||
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
|
||||||
|
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -334,6 +337,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
isSharded={false}
|
isSharded={false}
|
||||||
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
|
||||||
|
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -369,18 +369,21 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
</div>
|
</div>
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="OK"
|
buttonLabel="OK"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="OK"
|
text="OK"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="OK"
|
text="OK"
|
||||||
theme={
|
theme={
|
||||||
@@ -660,6 +663,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -941,6 +945,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -1223,6 +1228,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -5305,18 +5305,21 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="Execute"
|
buttonLabel="Execute"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="Execute"
|
ariaLabel="Execute"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Execute"
|
text="Execute"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="Execute"
|
ariaLabel="Execute"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Execute"
|
text="Execute"
|
||||||
theme={
|
theme={
|
||||||
@@ -5596,6 +5599,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="Execute"
|
ariaLabel="Execute"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -5877,6 +5881,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="Execute"
|
ariaLabel="Execute"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -6159,6 +6164,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="Execute"
|
ariaLabel="Execute"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ interface IGitHubReposPanelState {
|
|||||||
}
|
}
|
||||||
export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IGitHubReposPanelState> {
|
export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IGitHubReposPanelState> {
|
||||||
private static readonly PageSize = 30;
|
private static readonly PageSize = 30;
|
||||||
|
private static readonly MasterBranchName = "master";
|
||||||
|
private static readonly MainBranchName = "main";
|
||||||
|
|
||||||
private isAddedRepo = false;
|
private isAddedRepo = false;
|
||||||
private gitHubClient: GitHubClient;
|
private gitHubClient: GitHubClient;
|
||||||
private junoClient: JunoClient;
|
private junoClient: JunoClient;
|
||||||
@@ -116,6 +119,8 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
if (response.status !== HttpStatusCodes.OK) {
|
if (response.status !== HttpStatusCodes.OK) {
|
||||||
throw new Error(`Received HTTP ${response.status} when saving pinned repos`);
|
throw new Error(`Received HTTP ${response.status} when saving pinned repos`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.props.explorer.notebookManager?.refreshPinnedRepos();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
|
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
|
||||||
}
|
}
|
||||||
@@ -207,6 +212,14 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
if (response.data) {
|
if (response.data) {
|
||||||
branchesProps.branches = branchesProps.branches.concat(response.data);
|
branchesProps.branches = branchesProps.branches.concat(response.data);
|
||||||
branchesProps.lastPageInfo = response.pageInfo;
|
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) {
|
} catch (error) {
|
||||||
handleError(error, "GitHubReposPane/loadMoreBranches", "Failed to fetch branches");
|
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);
|
const existingRepo = this.pinnedReposProps.repos.find((repo) => repo.key === item.key);
|
||||||
if (existingRepo) {
|
if (existingRepo) {
|
||||||
existingRepo.branches = item.branches;
|
existingRepo.branches = item.branches;
|
||||||
|
this.setState({
|
||||||
|
gitHubReposState: {
|
||||||
|
...this.state.gitHubReposState,
|
||||||
|
reposListProps: {
|
||||||
|
...this.state.gitHubReposState.reposListProps,
|
||||||
|
pinnedReposProps: {
|
||||||
|
repos: this.pinnedReposProps.repos,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.pinnedReposProps.repos = [...this.pinnedReposProps.repos, item];
|
this.pinnedReposProps.repos = [...this.pinnedReposProps.repos, item];
|
||||||
}
|
}
|
||||||
@@ -374,6 +398,7 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
|
|||||||
lastPageInfo: undefined,
|
lastPageInfo: undefined,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
defaultBranchName: undefined,
|
||||||
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
|
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
|
||||||
};
|
};
|
||||||
this.loadMoreBranches(item.repo);
|
this.loadMoreBranches(item.repo);
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {},
|
"phoenixClient": PhoenixClient {
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
|||||||
@@ -3,12 +3,20 @@ import React from "react";
|
|||||||
|
|
||||||
export interface PanelFooterProps {
|
export interface PanelFooterProps {
|
||||||
buttonLabel: string;
|
buttonLabel: string;
|
||||||
|
isButtonDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ({
|
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ({
|
||||||
buttonLabel,
|
buttonLabel,
|
||||||
|
isButtonDisabled,
|
||||||
}: PanelFooterProps): JSX.Element => (
|
}: PanelFooterProps): JSX.Element => (
|
||||||
<div className="panelFooter">
|
<div className="panelFooter">
|
||||||
<PrimaryButton type="submit" id="sidePanelOkButton" text={buttonLabel} ariaLabel={buttonLabel} />
|
<PrimaryButton
|
||||||
|
type="submit"
|
||||||
|
id="sidePanelOkButton"
|
||||||
|
text={buttonLabel}
|
||||||
|
ariaLabel={buttonLabel}
|
||||||
|
disabled={!!isButtonDisabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface RightPaneFormProps {
|
|||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
submitButtonText: string;
|
submitButtonText: string;
|
||||||
isSubmitButtonHidden?: boolean;
|
isSubmitButtonHidden?: boolean;
|
||||||
|
isSubmitButtonDisabled?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
submitButtonText,
|
submitButtonText,
|
||||||
isSubmitButtonHidden = false,
|
isSubmitButtonHidden = false,
|
||||||
|
isSubmitButtonDisabled = false,
|
||||||
children,
|
children,
|
||||||
}: RightPaneFormProps) => {
|
}: RightPaneFormProps) => {
|
||||||
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
@@ -30,7 +32,9 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
|
|||||||
<form className="panelFormWrapper" onSubmit={handleOnSubmit}>
|
<form className="panelFormWrapper" onSubmit={handleOnSubmit}>
|
||||||
{formError && <PanelInfoErrorComponent messageType="error" message={formError} showErrorDetails={true} />}
|
{formError && <PanelInfoErrorComponent messageType="error" message={formError} showErrorDetails={true} />}
|
||||||
{children}
|
{children}
|
||||||
{!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />}
|
{!isSubmitButtonHidden && (
|
||||||
|
<PanelFooterComponent buttonLabel={submitButtonText} isButtonDisabled={isSubmitButtonDisabled} />
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
{isExecuting && <PanelLoadingScreen />}
|
{isExecuting && <PanelLoadingScreen />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -14,18 +14,21 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="Load"
|
buttonLabel="Load"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="Load"
|
ariaLabel="Load"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Load"
|
text="Load"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="Load"
|
ariaLabel="Load"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Load"
|
text="Load"
|
||||||
theme={
|
theme={
|
||||||
@@ -305,6 +308,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="Load"
|
ariaLabel="Load"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -586,6 +590,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="Load"
|
ariaLabel="Load"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -868,6 +873,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="Load"
|
ariaLabel="Load"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -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 * as Constants from "Common/Constants";
|
||||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
import { configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
@@ -113,20 +113,44 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
|
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
|
||||||
setPageOption(option.key);
|
setPageOption(option.key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const choiceButtonStyles = {
|
||||||
|
flexContainer: [
|
||||||
|
{
|
||||||
|
selectors: {
|
||||||
|
".ms-ChoiceField-wrapper label": {
|
||||||
|
fontSize: 12,
|
||||||
|
paddingTop: 0,
|
||||||
|
},
|
||||||
|
".ms-ChoiceField": {
|
||||||
|
marginTop: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...genericPaneProps}>
|
<RightPaneForm {...genericPaneProps}>
|
||||||
<div className="paneMainContent">
|
<div className="paneMainContent">
|
||||||
{shouldShowQueryPageOptions && (
|
{shouldShowQueryPageOptions && (
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart pageOptionsPart">
|
<div className="settingsSectionPart">
|
||||||
<div className="settingsSectionLabel">
|
<Stack horizontal>
|
||||||
Page options
|
<Text id="pageOptions" className="settingsSectionLabel" variant="small">
|
||||||
|
Page options
|
||||||
|
</Text>
|
||||||
<InfoTooltip>
|
<InfoTooltip>
|
||||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
|
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
|
||||||
query results per page.
|
query results per page.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</Stack>
|
||||||
<ChoiceGroup selectedKey={pageOption} options={pageOptionList} onChange={handleOnPageOptionChange} />
|
<ChoiceGroup
|
||||||
|
ariaLabelledBy="pageOptions"
|
||||||
|
selectedKey={pageOption}
|
||||||
|
options={pageOptionList}
|
||||||
|
styles={choiceButtonStyles}
|
||||||
|
onChange={handleOnPageOptionChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="tabs settingsSectionPart">
|
<div className="tabs settingsSectionPart">
|
||||||
{isCustomPageOptionSelected() && (
|
{isCustomPageOptionSelected() && (
|
||||||
|
|||||||
@@ -14,17 +14,24 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart pageOptionsPart"
|
className="settingsSectionPart"
|
||||||
>
|
>
|
||||||
<div
|
<Stack
|
||||||
className="settingsSectionLabel"
|
horizontal={true}
|
||||||
>
|
>
|
||||||
Page options
|
<Text
|
||||||
|
className="settingsSectionLabel"
|
||||||
|
id="pageOptions"
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
Page options
|
||||||
|
</Text>
|
||||||
<InfoTooltip>
|
<InfoTooltip>
|
||||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
|
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
|
||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
</div>
|
</Stack>
|
||||||
<StyledChoiceGroup
|
<StyledChoiceGroup
|
||||||
|
ariaLabelledBy="pageOptions"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
options={
|
options={
|
||||||
Array [
|
Array [
|
||||||
@@ -39,6 +46,23 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
selectedKey="custom"
|
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>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"phoenixClient": PhoenixClient {},
|
"phoenixClient": PhoenixClient {
|
||||||
|
"retryOptions": Object {
|
||||||
|
"maxTimeout": 5000,
|
||||||
|
"minTimeout": 5000,
|
||||||
|
"retries": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -675,18 +681,21 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="Create"
|
buttonLabel="Create"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="Create"
|
ariaLabel="Create"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Create"
|
text="Create"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="Create"
|
ariaLabel="Create"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Create"
|
text="Create"
|
||||||
theme={
|
theme={
|
||||||
@@ -966,6 +975,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="Create"
|
ariaLabel="Create"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -1247,6 +1257,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="Create"
|
ariaLabel="Create"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -1529,6 +1540,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="Create"
|
ariaLabel="Create"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -1262,18 +1262,21 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="OK"
|
buttonLabel="OK"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="OK"
|
text="OK"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="OK"
|
text="OK"
|
||||||
theme={
|
theme={
|
||||||
@@ -1553,6 +1556,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -1834,6 +1838,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -2116,6 +2121,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const {
|
|||||||
Inet,
|
Inet,
|
||||||
Smallint,
|
Smallint,
|
||||||
Tinyint,
|
Tinyint,
|
||||||
|
Timestamp,
|
||||||
} = TableConstants.CassandraType;
|
} = TableConstants.CassandraType;
|
||||||
export const cassandraOptions = [
|
export const cassandraOptions = [
|
||||||
{ key: Text, text: Text },
|
{ key: Text, text: Text },
|
||||||
@@ -50,6 +51,7 @@ export const cassandraOptions = [
|
|||||||
{ key: Inet, text: Inet },
|
{ key: Inet, text: Inet },
|
||||||
{ key: Smallint, text: Smallint },
|
{ key: Smallint, text: Smallint },
|
||||||
{ key: Tinyint, text: Tinyint },
|
{ key: Tinyint, text: Tinyint },
|
||||||
|
{ key: Timestamp, text: Timestamp },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const imageProps: IImageProps = {
|
export const imageProps: IImageProps = {
|
||||||
|
|||||||
@@ -356,18 +356,21 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="Add Entity"
|
buttonLabel="Add Entity"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="Add Entity"
|
ariaLabel="Add Entity"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Add Entity"
|
text="Add Entity"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="Add Entity"
|
ariaLabel="Add Entity"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Add Entity"
|
text="Add Entity"
|
||||||
theme={
|
theme={
|
||||||
@@ -647,6 +650,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="Add Entity"
|
ariaLabel="Add Entity"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -928,6 +932,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="Add Entity"
|
ariaLabel="Add Entity"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -1210,6 +1215,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="Add Entity"
|
ariaLabel="Add Entity"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -357,18 +357,21 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="Update"
|
buttonLabel="Update"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="Update"
|
ariaLabel="Update"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Update"
|
text="Update"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="Update"
|
ariaLabel="Update"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="Update"
|
text="Update"
|
||||||
theme={
|
theme={
|
||||||
@@ -648,6 +651,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="Update"
|
ariaLabel="Update"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -929,6 +933,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="Update"
|
ariaLabel="Update"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -1211,6 +1216,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="Update"
|
ariaLabel="Update"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -1041,18 +1041,21 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<PanelFooterComponent
|
<PanelFooterComponent
|
||||||
buttonLabel="OK"
|
buttonLabel="OK"
|
||||||
|
isButtonDisabled={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="panelFooter"
|
className="panelFooter"
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<CustomizedPrimaryButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="OK"
|
text="OK"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
text="OK"
|
text="OK"
|
||||||
theme={
|
theme={
|
||||||
@@ -1332,6 +1335,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
>
|
>
|
||||||
<CustomizedDefaultButton
|
<CustomizedDefaultButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -1613,6 +1617,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
>
|
>
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
@@ -1895,6 +1900,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
ariaLabel="OK"
|
ariaLabel="OK"
|
||||||
baseClassName="ms-Button"
|
baseClassName="ms-Button"
|
||||||
|
disabled={false}
|
||||||
id="sidePanelOkButton"
|
id="sidePanelOkButton"
|
||||||
onRenderDescription={[Function]}
|
onRenderDescription={[Function]}
|
||||||
primary={true}
|
primary={true}
|
||||||
|
|||||||
@@ -307,10 +307,18 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
iconSrc: AddDatabaseIcon,
|
iconSrc: AddDatabaseIcon,
|
||||||
title: "New " + getDatabaseName(),
|
title: "New " + getDatabaseName(),
|
||||||
description: undefined,
|
description: undefined,
|
||||||
onClick: () =>
|
onClick: async () => {
|
||||||
|
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||||
|
if (throughputCap && throughputCap !== -1) {
|
||||||
|
await useDatabases.getState().loadAllOffers();
|
||||||
|
}
|
||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this.container} />),
|
.openSidePanel(
|
||||||
|
"New " + getDatabaseName(),
|
||||||
|
<AddDatabasePanel explorer={this.container} buttonElement={document.activeElement as HTMLElement} />
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const CassandraType = {
|
|||||||
Float: "Float",
|
Float: "Float",
|
||||||
Int: "Int",
|
Int: "Int",
|
||||||
Text: "Text",
|
Text: "Text",
|
||||||
|
Timestamp: "Timestamp",
|
||||||
Uuid: "Uuid",
|
Uuid: "Uuid",
|
||||||
Varchar: "Varchar",
|
Varchar: "Varchar",
|
||||||
Varint: "Varint",
|
Varint: "Varint",
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
if (newHeaders.length > 0) {
|
if (newHeaders.length > 0) {
|
||||||
// Any new columns found will be added into headers array, which will trigger a re-render of the DataTable.
|
// 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.
|
// So there is no need to call it here.
|
||||||
this.updateHeaders(newHeaders, /* notifyColumnChanges */ true);
|
this.updateHeaders(selectedHeadersUnion, /* notifyColumnChanges */ true);
|
||||||
} else {
|
} else {
|
||||||
if (columnSortOrder) {
|
if (columnSortOrder) {
|
||||||
this.sortColumns(columnSortOrder, oSettings);
|
this.sortColumns(columnSortOrder, oSettings);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
@@ -32,6 +31,8 @@ export interface CassandraTableKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class TableDataClient {
|
export abstract class TableDataClient {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
public abstract createDocument(
|
public abstract createDocument(
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
entity: Entities.ITableEntity
|
entity: Entities.ITableEntity
|
||||||
@@ -53,7 +54,7 @@ export abstract class TableDataClient {
|
|||||||
public abstract deleteDocuments(
|
public abstract deleteDocuments(
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
entitiesToDelete: Entities.ITableEntity[]
|
entitiesToDelete: Entities.ITableEntity[]
|
||||||
): Promise<unknown>;
|
): Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TablesAPIDataClient extends TableDataClient {
|
export class TablesAPIDataClient extends TableDataClient {
|
||||||
@@ -66,7 +67,7 @@ export class TablesAPIDataClient extends TableDataClient {
|
|||||||
collection,
|
collection,
|
||||||
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
|
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
|
||||||
).then(
|
).then(
|
||||||
(newDocument: unknown) => {
|
(newDocument: any) => {
|
||||||
const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
|
const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
|
||||||
deferred.resolve(newEntity);
|
deferred.resolve(newEntity);
|
||||||
},
|
},
|
||||||
@@ -145,7 +146,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
const clearInProgressMessage = logConsoleProgress(`Adding new row to table ${collection.id()}`);
|
const clearInProgressMessage = logConsoleProgress(`Adding new row to table ${collection.id()}`);
|
||||||
let properties = "(";
|
let properties = "(";
|
||||||
let values = "(";
|
let values = "(";
|
||||||
for (const property in entity) {
|
for (let property in entity) {
|
||||||
if (entity[property]._ === null) {
|
if (entity[property]._ === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -163,7 +164,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
const deferred = Q.defer<Entities.ITableEntity>();
|
const deferred = Q.defer<Entities.ITableEntity>();
|
||||||
this.queryDocuments(collection, query)
|
this.queryDocuments(collection, query)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
(data: any) => {
|
||||||
entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)];
|
entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)];
|
||||||
entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString();
|
entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString();
|
||||||
logConsoleInfo(`Successfully added new row to table ${collection.id()}`);
|
logConsoleInfo(`Successfully added new row to table ${collection.id()}`);
|
||||||
@@ -187,10 +188,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let whereSegment = " WHERE";
|
let whereSegment = " WHERE";
|
||||||
const keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat(
|
let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat(
|
||||||
collection.cassandraKeys.clusteringKeys
|
collection.cassandraKeys.clusteringKeys
|
||||||
);
|
);
|
||||||
for (const keyIndex in keys) {
|
for (let keyIndex in keys) {
|
||||||
const key = keys[keyIndex].property;
|
const key = keys[keyIndex].property;
|
||||||
const keyType = keys[keyIndex].type;
|
const keyType = keys[keyIndex].type;
|
||||||
whereSegment += this.isStringType(keyType)
|
whereSegment += this.isStringType(keyType)
|
||||||
@@ -202,7 +203,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`;
|
let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`;
|
||||||
let isPropertyUpdated = false;
|
let isPropertyUpdated = false;
|
||||||
let isFirstPropertyToUpdate = true;
|
let isFirstPropertyToUpdate = true;
|
||||||
for (const property in newEntity) {
|
for (let property in newEntity) {
|
||||||
if (
|
if (
|
||||||
!originalDocument[property] ||
|
!originalDocument[property] ||
|
||||||
newEntity[property]._.toString() !== originalDocument[property]._.toString()
|
newEntity[property]._.toString() !== originalDocument[property]._.toString()
|
||||||
@@ -228,7 +229,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
|
|
||||||
let deleteQuery = `DELETE `;
|
let deleteQuery = `DELETE `;
|
||||||
let isPropertyDeleted = false;
|
let isPropertyDeleted = false;
|
||||||
for (const property in originalDocument) {
|
for (let property in originalDocument) {
|
||||||
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
|
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
|
||||||
deleteQuery += ` ${property},`;
|
deleteQuery += ` ${property},`;
|
||||||
isPropertyDeleted = true;
|
isPropertyDeleted = true;
|
||||||
@@ -332,16 +333,16 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
resourceId: string,
|
resourceId: string,
|
||||||
explorer: Explorer,
|
explorer: Explorer,
|
||||||
createKeyspaceQuery: string
|
createKeyspaceQuery: string
|
||||||
): Q.Promise<unknown> {
|
): Q.Promise<any> {
|
||||||
if (!createKeyspaceQuery) {
|
if (!createKeyspaceQuery) {
|
||||||
return Q.reject("No query specified");
|
return Q.reject("No query specified");
|
||||||
}
|
}
|
||||||
|
|
||||||
const deferred: Q.Deferred<unknown> = Q.defer();
|
const deferred: Q.Deferred<any> = Q.defer();
|
||||||
const clearInProgressMessage = logConsoleProgress(`Creating a new keyspace with query ${createKeyspaceQuery}`);
|
const clearInProgressMessage = logConsoleProgress(`Creating a new keyspace with query ${createKeyspaceQuery}`);
|
||||||
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery)
|
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
(data: any) => {
|
||||||
logConsoleInfo(`Successfully created a keyspace with query ${createKeyspaceQuery}`);
|
logConsoleInfo(`Successfully created a keyspace with query ${createKeyspaceQuery}`);
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
},
|
},
|
||||||
@@ -365,8 +366,8 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
explorer: Explorer,
|
explorer: Explorer,
|
||||||
createTableQuery: string,
|
createTableQuery: string,
|
||||||
createKeyspaceQuery?: string
|
createKeyspaceQuery?: string
|
||||||
): Q.Promise<unknown> {
|
): Q.Promise<any> {
|
||||||
let createKeyspacePromise: Q.Promise<unknown>;
|
let createKeyspacePromise: Q.Promise<any>;
|
||||||
if (createKeyspaceQuery) {
|
if (createKeyspaceQuery) {
|
||||||
createKeyspacePromise = this.createKeyspace(cassandraEndpoint, resourceId, explorer, createKeyspaceQuery);
|
createKeyspacePromise = this.createKeyspace(cassandraEndpoint, resourceId, explorer, createKeyspaceQuery);
|
||||||
} else {
|
} else {
|
||||||
@@ -379,7 +380,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
const clearInProgressMessage = logConsoleProgress(`Creating a new table with query ${createTableQuery}`);
|
const clearInProgressMessage = logConsoleProgress(`Creating a new table with query ${createTableQuery}`);
|
||||||
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery)
|
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
(data: any) => {
|
||||||
logConsoleInfo(`Successfully created a table with query ${createTableQuery}`);
|
logConsoleInfo(`Successfully created a table with query ${createTableQuery}`);
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
},
|
},
|
||||||
@@ -398,7 +399,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||||
if (collection.cassandraKeys) {
|
if (!!collection.cassandraKeys) {
|
||||||
return Q.resolve(collection.cassandraKeys);
|
return Q.resolve(collection.cassandraKeys);
|
||||||
}
|
}
|
||||||
const clearInProgressMessage = logConsoleProgress(`Fetching keys for table ${collection.id()}`);
|
const clearInProgressMessage = logConsoleProgress(`Fetching keys for table ${collection.id()}`);
|
||||||
@@ -407,7 +408,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
authType === AuthType.EncryptedToken
|
authType === AuthType.EncryptedToken
|
||||||
? Constants.CassandraBackend.guestKeysApi
|
? Constants.CassandraBackend.guestKeysApi
|
||||||
: Constants.CassandraBackend.keysApi;
|
: Constants.CassandraBackend.keysApi;
|
||||||
const endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||||
const deferred = Q.defer<CassandraTableKeys>();
|
const deferred = Q.defer<CassandraTableKeys>();
|
||||||
|
|
||||||
$.ajax(endpoint, {
|
$.ajax(endpoint, {
|
||||||
@@ -428,7 +429,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
logConsoleInfo(`Successfully fetched keys for table ${collection.id()}`);
|
logConsoleInfo(`Successfully fetched keys for table ${collection.id()}`);
|
||||||
deferred.resolve(data);
|
deferred.resolve(data);
|
||||||
},
|
},
|
||||||
(error: Error) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
@@ -438,7 +439,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||||
if (collection.cassandraSchema) {
|
if (!!collection.cassandraSchema) {
|
||||||
return Q.resolve(collection.cassandraSchema);
|
return Q.resolve(collection.cassandraSchema);
|
||||||
}
|
}
|
||||||
const clearInProgressMessage = logConsoleProgress(`Fetching schema for table ${collection.id()}`);
|
const clearInProgressMessage = logConsoleProgress(`Fetching schema for table ${collection.id()}`);
|
||||||
@@ -447,7 +448,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
authType === AuthType.EncryptedToken
|
authType === AuthType.EncryptedToken
|
||||||
? Constants.CassandraBackend.guestSchemaApi
|
? Constants.CassandraBackend.guestSchemaApi
|
||||||
: Constants.CassandraBackend.schemaApi;
|
: Constants.CassandraBackend.schemaApi;
|
||||||
const endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||||
const deferred = Q.defer<CassandraTableKey[]>();
|
const deferred = Q.defer<CassandraTableKey[]>();
|
||||||
|
|
||||||
$.ajax(endpoint, {
|
$.ajax(endpoint, {
|
||||||
@@ -468,7 +469,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
logConsoleInfo(`Successfully fetched schema for table ${collection.id()}`);
|
logConsoleInfo(`Successfully fetched schema for table ${collection.id()}`);
|
||||||
deferred.resolve(data.columns);
|
deferred.resolve(data.columns);
|
||||||
},
|
},
|
||||||
(error: Error) => {
|
(error: any) => {
|
||||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
@@ -495,7 +496,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
beforeSend: this.setAuthorizationHeader,
|
beforeSend: this.setAuthorizationHeader,
|
||||||
cache: false,
|
cache: false,
|
||||||
}).then(
|
}).then(
|
||||||
() => {
|
(data: any) => {
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
},
|
},
|
||||||
(reason) => {
|
(reason) => {
|
||||||
@@ -534,7 +535,8 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
dataType === TableConstants.CassandraType.Text ||
|
dataType === TableConstants.CassandraType.Text ||
|
||||||
dataType === TableConstants.CassandraType.Inet ||
|
dataType === TableConstants.CassandraType.Inet ||
|
||||||
dataType === TableConstants.CassandraType.Ascii ||
|
dataType === TableConstants.CassandraType.Ascii ||
|
||||||
dataType === TableConstants.CassandraType.Varchar
|
dataType === TableConstants.CassandraType.Varchar ||
|
||||||
|
dataType === TableConstants.CassandraType.Timestamp
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
* 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 {
|
export default class NotebookTabBase extends TabsBase {
|
||||||
protected static clientManager: NotebookClientV2;
|
protected static clientManager: NotebookClientV2;
|
||||||
@@ -27,6 +28,15 @@ export default class NotebookTabBase extends TabsBase {
|
|||||||
|
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
|
|
||||||
|
useNotebook.subscribe(
|
||||||
|
() => {
|
||||||
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint) {
|
||||||
|
NotebookTabBase.clientManager = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(state) => state.notebookServerInfo
|
||||||
|
);
|
||||||
if (!NotebookTabBase.clientManager) {
|
if (!NotebookTabBase.clientManager) {
|
||||||
NotebookTabBase.clientManager = new NotebookClientV2({
|
NotebookTabBase.clientManager = new NotebookClientV2({
|
||||||
connectionInfo: useNotebook.getState().notebookServerInfo,
|
connectionInfo: useNotebook.getState().notebookServerInfo,
|
||||||
|
|||||||
@@ -53,14 +53,16 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
onUpdateKernelInfo: this.onKernelUpdate,
|
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 = () => {
|
const cleanup = () => {
|
||||||
this.notebookComponentAdapter.notebookShutdown();
|
this.notebookComponentAdapter.notebookShutdown();
|
||||||
super.onCloseTabButtonClick();
|
super.onCloseTabButtonClick();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.notebookComponentAdapter.isContentDirty()) {
|
if (this.notebookComponentAdapter.isContentDirty() && hardClose === false) {
|
||||||
useDialog
|
useDialog
|
||||||
.getState()
|
.getState()
|
||||||
.showOkCancelModalDialog(
|
.showOkCancelModalDialog(
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export default class TerminalTab extends TabsBase {
|
|||||||
return {
|
return {
|
||||||
authToken: info.authToken,
|
authToken: info.authToken,
|
||||||
notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`,
|
notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`,
|
||||||
|
forwardingId: info.forwardingId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
|
import { NotebookUtil } from "Explorer/Notebook/NotebookUtil";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -528,6 +529,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onSchemaAnalyzerClick = async () => {
|
public onSchemaAnalyzerClick = async () => {
|
||||||
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
await this.container.allocateContainer();
|
||||||
|
}
|
||||||
useSelectedNode.getState().setSelectedNode(this);
|
useSelectedNode.getState().setSelectedNode(this);
|
||||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
|
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
|
||||||
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
|
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
|
||||||
@@ -572,7 +576,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
|
|
||||||
public onSettingsClick = async (): Promise<void> => {
|
public onSettingsClick = async (): Promise<void> => {
|
||||||
useSelectedNode.getState().setSelectedNode(this);
|
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);
|
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
|
||||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
description: "Settings node",
|
description: "Settings node",
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
this.isOfferRead = false;
|
this.isOfferRead = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSettingsClick = (): void => {
|
public onSettingsClick = async (): Promise<void> => {
|
||||||
useSelectedNode.getState().setSelectedNode(this);
|
useSelectedNode.getState().setSelectedNode(this);
|
||||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
||||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||||
@@ -66,6 +66,11 @@ export default class Database implements ViewModels.Database {
|
|||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
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 pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification();
|
||||||
const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2;
|
const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2;
|
||||||
const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id());
|
const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id());
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface DatabasesState {
|
|||||||
findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection;
|
findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection;
|
||||||
isLastCollection: () => boolean;
|
isLastCollection: () => boolean;
|
||||||
loadDatabaseOffers: () => Promise<void>;
|
loadDatabaseOffers: () => Promise<void>;
|
||||||
|
loadAllOffers: () => Promise<void>;
|
||||||
isFirstResourceCreated: () => boolean;
|
isFirstResourceCreated: () => boolean;
|
||||||
findSelectedDatabase: () => ViewModels.Database;
|
findSelectedDatabase: () => ViewModels.Database;
|
||||||
validateDatabaseId: (id: string) => boolean;
|
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: () => {
|
isFirstResourceCreated: () => {
|
||||||
const databases = get().databases;
|
const databases = get().databases;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ConnectionStatusType } from "Common/Constants";
|
||||||
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
@@ -12,6 +14,7 @@ export interface SelectedNodeState {
|
|||||||
collectionId?: string,
|
collectionId?: string,
|
||||||
subnodeKinds?: ViewModels.CollectionTabKind[]
|
subnodeKinds?: ViewModels.CollectionTabKind[]
|
||||||
) => boolean;
|
) => boolean;
|
||||||
|
isConnectedToContainer: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
|
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
|
||||||
@@ -59,4 +62,7 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
|
|||||||
subnodeKinds.includes(selectedSubnodeKind)
|
subnodeKinds.includes(selectedSubnodeKind)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isConnectedToContainer: (): boolean => {
|
||||||
|
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import postRobot from "post-robot";
|
import postRobot from "post-robot";
|
||||||
|
import { GetGithubClientId } from "Utils/GitHubUtils";
|
||||||
import { HttpStatusCodes } from "../Common/Constants";
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
import { handleError } from "../Common/ErrorHandlingUtils";
|
import { handleError } from "../Common/ErrorHandlingUtils";
|
||||||
import { configContext } from "../ConfigContext";
|
|
||||||
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
||||||
import { JunoClient } from "../Juno/JunoClient";
|
import { JunoClient } from "../Juno/JunoClient";
|
||||||
import { logConsoleInfo } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo } from "../Utils/NotificationConsoleUtils";
|
||||||
@@ -55,7 +55,7 @@ export class GitHubOAuthService {
|
|||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
scope,
|
scope,
|
||||||
client_id: configContext.GITHUB_CLIENT_ID,
|
client_id: GetGithubClientId(),
|
||||||
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
|
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
|
||||||
state: this.resetState(),
|
state: this.resetState(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
|
import { GetGithubClientId } from "Utils/GitHubUtils";
|
||||||
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
@@ -522,7 +523,7 @@ export class JunoClient {
|
|||||||
|
|
||||||
private static getGitHubClientParams(): URLSearchParams {
|
private static getGitHubClientParams(): URLSearchParams {
|
||||||
const githubParams = new URLSearchParams({
|
const githubParams = new URLSearchParams({
|
||||||
client_id: configContext.GITHUB_CLIENT_ID,
|
client_id: GetGithubClientId(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (configContext.GITHUB_CLIENT_SECRET) {
|
if (configContext.GITHUB_CLIENT_SECRET) {
|
||||||
|
|||||||
@@ -1,33 +1,46 @@
|
|||||||
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
|
import { ContainerStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../Common/Constants";
|
||||||
|
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||||
|
import * as Logger from "../Common/Logger";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
|
import {
|
||||||
|
ContainerInfo,
|
||||||
|
IContainerData,
|
||||||
|
IPhoenixConnectionInfoResult,
|
||||||
|
IProvisionData,
|
||||||
|
IResponse,
|
||||||
|
} from "../Contracts/DataModels";
|
||||||
|
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
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 {
|
export class PhoenixClient {
|
||||||
public async containerConnectionInfo(
|
private containerHealthHandler: NodeJS.Timeout;
|
||||||
provisionData: IProvosionData
|
private retryOptions: promiseRetry.Options = {
|
||||||
): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> {
|
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 {
|
try {
|
||||||
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/allocate`, {
|
const response = await fetch(`${this.getPhoenixContainerPoolingEndPoint()}/${operation}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: PhoenixClient.getHeaders(),
|
headers: PhoenixClient.getHeaders(),
|
||||||
body: JSON.stringify(provisionData),
|
body: JSON.stringify(provisionData),
|
||||||
});
|
});
|
||||||
|
|
||||||
let data: IPhoenixConnectionInfoResult;
|
let data: IPhoenixConnectionInfoResult;
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
data = await response.json();
|
data = await response.json();
|
||||||
@@ -42,6 +55,68 @@ export class PhoenixClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async initiateContainerHeartBeat(containerData: { forwardingId: string; dbAccountName: string }) {
|
||||||
|
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.getPhoenixContainerPoolingEndPoint()}/${containerData.dbAccountName}/${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) {
|
||||||
|
throw new AbortError(response.statusText);
|
||||||
|
}
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
};
|
||||||
|
return await promiseRetry(runContainerStatusAsync, this.retryOptions);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(getErrorMessage(error), "PhoenixClient/getContainerStatus");
|
||||||
|
return {
|
||||||
|
durationLeftInMinutes: undefined,
|
||||||
|
notebookServerInfo: undefined,
|
||||||
|
status: ContainerStatusType.Disconnected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getContainerHealth(delayMs: number, containerData: { forwardingId: string; dbAccountName: string }) {
|
||||||
|
try {
|
||||||
|
const containerInfo = await this.getContainerStatusAsync(containerData);
|
||||||
|
useNotebook.getState().setContainerStatus(containerInfo);
|
||||||
|
if (useNotebook.getState().containerStatus?.status === ContainerStatusType.Active) {
|
||||||
|
this.scheduleContainerHeartbeat(delayMs, containerData);
|
||||||
|
}
|
||||||
|
} catch (exception) {
|
||||||
|
useNotebook.getState().setContainerStatus({
|
||||||
|
durationLeftInMinutes: undefined,
|
||||||
|
notebookServerInfo: undefined,
|
||||||
|
status: ContainerStatusType.Disconnected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static getPhoenixEndpoint(): string {
|
public static getPhoenixEndpoint(): string {
|
||||||
const phoenixEndpoint =
|
const phoenixEndpoint =
|
||||||
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||||
@@ -57,6 +132,7 @@ export class PhoenixClient {
|
|||||||
public getPhoenixContainerPoolingEndPoint(): string {
|
public getPhoenixContainerPoolingEndPoint(): string {
|
||||||
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer`;
|
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getHeaders(): HeadersInit {
|
private static getHeaders(): HeadersInit {
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type Features = {
|
|||||||
partitionKeyDefault: boolean;
|
partitionKeyDefault: boolean;
|
||||||
partitionKeyDefault2: boolean;
|
partitionKeyDefault2: boolean;
|
||||||
phoenix: boolean;
|
phoenix: boolean;
|
||||||
|
notebooksDownBanner: boolean;
|
||||||
readonly enableSDKoperations: boolean;
|
readonly enableSDKoperations: boolean;
|
||||||
readonly enableSpark: boolean;
|
readonly enableSpark: boolean;
|
||||||
readonly enableTtl: boolean;
|
readonly enableTtl: boolean;
|
||||||
@@ -33,6 +34,7 @@ export type Features = {
|
|||||||
readonly mongoProxyEndpoint?: string;
|
readonly mongoProxyEndpoint?: string;
|
||||||
readonly mongoProxyAPIs?: string;
|
readonly mongoProxyAPIs?: string;
|
||||||
readonly notebooksTemporarilyDown: boolean;
|
readonly notebooksTemporarilyDown: boolean;
|
||||||
|
readonly enableThroughputCap: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
|
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
|
||||||
@@ -84,6 +86,8 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
|
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
|
||||||
notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"),
|
notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"),
|
||||||
phoenix: "true" === get("phoenix"),
|
phoenix: "true" === get("phoenix"),
|
||||||
|
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
||||||
|
enableThroughputCap: "true" === get("enablethroughputcap"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { RefreshResult } from "../SelfServeTypes";
|
|||||||
import SqlX from "./SqlX";
|
import SqlX from "./SqlX";
|
||||||
import {
|
import {
|
||||||
FetchPricesResponse,
|
FetchPricesResponse,
|
||||||
|
PriceMapAndCurrencyCode,
|
||||||
RegionsResponse,
|
RegionsResponse,
|
||||||
SqlxServiceResource,
|
SqlxServiceResource,
|
||||||
UpdateDedicatedGatewayRequestParameters,
|
UpdateDedicatedGatewayRequestParameters,
|
||||||
@@ -178,18 +179,18 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
|||||||
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
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 = {
|
const telemetryData = {
|
||||||
feature: "Calculate approximate cost",
|
feature: "Calculate approximate cost",
|
||||||
function: "getPriceMap",
|
function: "getPriceMapAndCurrencyCode",
|
||||||
description: "fetch prices API call",
|
description: "fetch prices API call",
|
||||||
selfServeClassName: SqlX.name,
|
selfServeClassName: SqlX.name,
|
||||||
};
|
};
|
||||||
const getPriceMapTimestamp = selfServeTraceStart(telemetryData);
|
const getPriceMapAndCurrencyCodeTimestamp = selfServeTraceStart(telemetryData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const priceMap = new Map<string, Map<string, number>>();
|
const priceMap = new Map<string, Map<string, number>>();
|
||||||
|
let currencyCode;
|
||||||
for (const region of regions) {
|
for (const region of regions) {
|
||||||
const regionPriceMap = new Map<string, number>();
|
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) {
|
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);
|
regionPriceMap.set(item.skuName, item.retailPrice);
|
||||||
}
|
}
|
||||||
priceMap.set(region, regionPriceMap);
|
priceMap.set(region, regionPriceMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
selfServeTraceSuccess(telemetryData, getPriceMapTimestamp);
|
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
|
||||||
return priceMap;
|
return { priceMap: priceMap, currencyCode: currencyCode };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||||
selfServeTraceFailure(failureTelemetry, getPriceMapTimestamp);
|
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
||||||
|
return { priceMap: undefined, currencyCode: undefined };
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils";
|
|||||||
import {
|
import {
|
||||||
deleteDedicatedGatewayResource,
|
deleteDedicatedGatewayResource,
|
||||||
getCurrentProvisioningState,
|
getCurrentProvisioningState,
|
||||||
getPriceMap,
|
getPriceMapAndCurrencyCode,
|
||||||
getRegions,
|
getRegions,
|
||||||
refreshDedicatedGatewayProvisioning,
|
refreshDedicatedGatewayProvisioning,
|
||||||
updateDedicatedGatewayResource,
|
updateDedicatedGatewayResource,
|
||||||
@@ -207,6 +207,7 @@ const ApproximateCostDropDownInfo: Info = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let priceMap: Map<string, Map<string, number>>;
|
let priceMap: Map<string, Map<string, number>>;
|
||||||
|
let currencyCode: string;
|
||||||
let regions: Array<string>;
|
let regions: Array<string>;
|
||||||
|
|
||||||
const calculateCost = (skuName: string, instanceCount: number): Description => {
|
const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||||
@@ -237,7 +238,7 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
|||||||
|
|
||||||
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
|
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
|
||||||
return {
|
return {
|
||||||
textTKey: `${costPerHour} USD`,
|
textTKey: `${costPerHour} ${currencyCode}`,
|
||||||
type: DescriptionType.Text,
|
type: DescriptionType.Text,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -346,7 +347,9 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
});
|
});
|
||||||
|
|
||||||
regions = await getRegions();
|
regions = await getRegions();
|
||||||
priceMap = await getPriceMap(regions);
|
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(regions);
|
||||||
|
priceMap = priceMapAndCurrencyCode.priceMap;
|
||||||
|
currencyCode = priceMapAndCurrencyCode.currencyCode;
|
||||||
|
|
||||||
const response = await getCurrentProvisioningState();
|
const response = await getCurrentProvisioningState();
|
||||||
if (response.status && response.status !== "Deleting") {
|
if (response.status && response.status !== "Deleting") {
|
||||||
|
|||||||
@@ -36,9 +36,15 @@ export type FetchPricesResponse = {
|
|||||||
Count: number;
|
Count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PriceMapAndCurrencyCode = {
|
||||||
|
priceMap: Map<string, Map<string, number>>;
|
||||||
|
currencyCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PriceItem = {
|
export type PriceItem = {
|
||||||
retailPrice: number;
|
retailPrice: number;
|
||||||
skuName: string;
|
skuName: string;
|
||||||
|
currencyCode: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RegionsResponse = {
|
export type RegionsResponse = {
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export enum Action {
|
|||||||
SubscriptionSwitch,
|
SubscriptionSwitch,
|
||||||
TenantSwitch,
|
TenantSwitch,
|
||||||
DefaultTenantSwitch,
|
DefaultTenantSwitch,
|
||||||
ResetNotebookWorkspace,
|
|
||||||
CreateNotebookWorkspace,
|
CreateNotebookWorkspace,
|
||||||
NotebookErrorNotification,
|
NotebookErrorNotification,
|
||||||
CreateSparkCluster,
|
CreateSparkCluster,
|
||||||
@@ -82,6 +81,8 @@ export enum Action {
|
|||||||
NotebooksInsertTextCellBelowFromMenu,
|
NotebooksInsertTextCellBelowFromMenu,
|
||||||
NotebooksMoveCellUpFromMenu,
|
NotebooksMoveCellUpFromMenu,
|
||||||
NotebooksMoveCellDownFromMenu,
|
NotebooksMoveCellDownFromMenu,
|
||||||
|
PhoenixConnection,
|
||||||
|
PhoenixResetWorkspace,
|
||||||
DeleteCellFromMenu,
|
DeleteCellFromMenu,
|
||||||
OpenTerminal,
|
OpenTerminal,
|
||||||
CreateMongoCollectionWithWildcardIndex,
|
CreateMongoCollectionWithWildcardIndex,
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ export function downloadItem(
|
|||||||
useDialog
|
useDialog
|
||||||
.getState()
|
.getState()
|
||||||
.showOkModalDialog(
|
.showOkModalDialog(
|
||||||
"Failed to Connect",
|
"Failed to connect",
|
||||||
"Failed to connect to temporary workspace. Please refresh the page and try again."
|
"Failed to connect to temporary workspace. Please refresh the page and try again."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
// https://github.com/<owner>/<repo>/tree/<branch>
|
// 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
|
// The url when users visit a repo/branch on github.com
|
||||||
export const RepoUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/tree\/([^?]*)/;
|
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 {
|
export function toRawContentUri(owner: string, repo: string, branch: string, path: string): string {
|
||||||
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
interface AccountListResult {
|
interface AccountListResult {
|
||||||
nextLink: string;
|
nextLink: string;
|
||||||
@@ -14,8 +15,8 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken:
|
|||||||
headers.append("Authorization", bearer);
|
headers.append("Authorization", bearer);
|
||||||
|
|
||||||
let accounts: Array<DatabaseAccount> = [];
|
let accounts: Array<DatabaseAccount> = [];
|
||||||
|
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=2021-06-15`;
|
let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=${apiVersion}`;
|
||||||
|
|
||||||
while (nextLink) {
|
while (nextLink) {
|
||||||
const response: Response = await fetch(nextLink, { headers });
|
const response: Response = await fetch(nextLink, { headers });
|
||||||
|
|||||||
@@ -342,6 +342,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
if (inputs.flights.indexOf(Flights.Phoenix) !== -1) {
|
if (inputs.flights.indexOf(Flights.Phoenix) !== -1) {
|
||||||
userContext.features.phoenix = true;
|
userContext.features.phoenix = true;
|
||||||
}
|
}
|
||||||
|
if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) {
|
||||||
|
userContext.features.notebooksDownBanner = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
import { CollectionTabKind } from "../Contracts/ViewModels";
|
||||||
|
import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
|
||||||
import TabsBase from "../Explorer/Tabs/TabsBase";
|
import TabsBase from "../Explorer/Tabs/TabsBase";
|
||||||
|
|
||||||
interface TabsState {
|
interface TabsState {
|
||||||
@@ -12,6 +14,7 @@ interface TabsState {
|
|||||||
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
|
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
|
||||||
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
|
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
|
||||||
closeTab: (tab: TabsBase) => void;
|
closeTab: (tab: TabsBase) => void;
|
||||||
|
closeAllNotebookTabs: (hardClose: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||||
@@ -78,4 +81,31 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
|
|
||||||
set({ openedTabs: updatedTabs });
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user