Compare commits

...

33 Commits

Author SHA1 Message Date
kcheekuri
0fe1e2940e Resolve compile errors 2021-11-10 14:22:30 -05:00
kcheekuri
f9d91061a7 Change endpoint 2021-11-10 12:56:08 -05:00
kcheekuri
4a50d138cd Merge branch 'master' into users/kcheekuri/removePhoenixFlag 2021-11-10 11:39:41 -05:00
kcheekuri
a7409553d4 Remove phoenix and notebook temporary down features and apply phoenix for whitelisted acounts 2021-11-10 10:52:41 -05:00
tarazou9
27a49e9aa9 add juno test3 to allow list (#1158)
* add juno test3 to allow list

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

* format issue

* Address comments

* Test Check

* Added await

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

* formatting & linting fixes

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

* undid config context changes

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

* Format and Lint issues

* Typo issue

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

* Closing only notebook related

* resolved comments from previous PR

* On Schema Analyser allocate call

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

* git connection issue

* format issue

* Typo issue

* added constants

* Removed math.round for remainingTime

* code refctor for container status check

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

* lint and format fixes

* modified shell enabled

* added notebooks down banner flight

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

* lint and format fixes

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

This reverts commit f7e7240010.

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

This reverts commit ed1ffb692f.
2021-10-15 17:36:48 -07:00
victor-meng
9f27cb95b9 Only use the SET keyword once in the update query (#1138) 2021-10-15 12:33:59 -07:00
Hardikkumar Nai
271256bffb resolve_eslint_NodePropertiesComponent (#921)
* resolve_eslint_NodePropertiesComponent

* address commit

* Open new screen: Screen reader does not pass the 'Copied' information after selecting 'Copy' button.

* resolve lint error
2021-10-12 08:43:35 -07:00
vaidankarswapnil
aff7133095 Fix eslint issues for TableCommands and other files (#1132) 2021-10-12 08:07:06 -07:00
Hardikkumar Nai
bfd4948fb9 absulte_path setting (#984)
* absulte_path setting

* resolve build time error
2021-10-12 07:38:34 -07:00
victor-meng
1c54459708 If unsharded is checked, set partition key to undefined (#1128) 2021-10-11 12:09:38 -07:00
Sunil Kumar Yadav
df3b18d585 fixed eslint of NotebookComponentBootstrapper and NotebookReadOnlyRenderer (#1122) 2021-10-11 08:29:21 -07:00
Sunil Kumar Yadav
882f0e1554 fixed GraphExplorer.tsx ellint issue (#1124) 2021-10-11 08:21:52 -07:00
vaidankarswapnil
b67b76cc87 Fix eslint issues for QueryBuilderViewModal and QueryClauseViewModel (#1125) 2021-10-11 08:20:38 -07:00
vaidankarswapnil
734ee1e436 Fix eslint issues for ClauseGroup and ClauseGroupViewModel files (#1127) 2021-10-11 07:56:12 -07:00
Sunil Kumar Yadav
ff498b51e2 fixed eslint of Trigger.ts GithubOAuthService.ts etc (#1126) 2021-10-11 07:55:21 -07:00
vaidankarswapnil
ed1ffb692f Fix ally database panel open issue (#1120) 2021-10-06 07:53:46 -07:00
Karthik chakravarthy
f7fa3f7c09 Fix Unit Test: Mock the class to its instance (#1117)
* mock to instance

* Update jest.config.js

Co-authored-by: Jordi Bunster <jbunster@microsoft.com>
2021-10-05 13:06:26 -07:00
Jordi Bunster
6ebf19c0c9 Close tab fixes (#1107)
* Close tab fixes

Ensure that when promoting a new tab to being the 'active' tab
(as a consequence of, say, closing the active tab) that the newly
promoted tab has a chance to install its buttons and what not.

* Set new active tab even if undefined
2021-10-05 09:25:35 -07:00
Karthik chakravarthy
f968f57543 Allocation of container on demand (#1097)
* Allocation of container only on demand

* git notebook click emits connect to container and refresh notebooks

* git cleanup local repo

* Reconnect rename and memory heartbeat change

* Fix new notebook click event in case of git login

* Mongo proxy test replace with master file

* code refactor

* format check

* compile errors resolved

* address review comments

* rename environment to workspace

* Added content html rendering in Dialog

* format issue

* Added contentHtml in the dialog which renders jsx element
2021-09-30 12:53:33 -04:00
96 changed files with 1901 additions and 897 deletions

View File

@@ -81,17 +81,9 @@ src/Explorer/Tables/DataTable/DataTableBindingManager.ts
src/Explorer/Tables/DataTable/DataTableBuilder.ts src/Explorer/Tables/DataTable/DataTableBuilder.ts
src/Explorer/Tables/DataTable/DataTableContextMenu.ts src/Explorer/Tables/DataTable/DataTableContextMenu.ts
src/Explorer/Tables/DataTable/DataTableOperationManager.ts src/Explorer/Tables/DataTable/DataTableOperationManager.ts
src/Explorer/Tables/DataTable/DataTableOperations.ts
src/Explorer/Tables/DataTable/DataTableViewModel.ts src/Explorer/Tables/DataTable/DataTableViewModel.ts
src/Explorer/Tables/DataTable/TableCommands.ts
src/Explorer/Tables/DataTable/TableEntityCache.ts
src/Explorer/Tables/DataTable/TableEntityListViewModel.ts src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
src/Explorer/Tables/Entities.ts
src/Explorer/Tables/QueryBuilder/ClauseGroup.ts
src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
src/Explorer/Tables/TableDataClient.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
@@ -115,15 +107,10 @@ src/Explorer/Tree/ObjectId.ts
src/Explorer/Tree/ResourceTokenCollection.ts src/Explorer/Tree/ResourceTokenCollection.ts
src/Explorer/Tree/StoredProcedure.ts src/Explorer/Tree/StoredProcedure.ts
src/Explorer/Tree/TreeComponents.ts src/Explorer/Tree/TreeComponents.ts
src/Explorer/Tree/Trigger.ts
src/Explorer/WaitsForTemplateViewModel.ts src/Explorer/WaitsForTemplateViewModel.ts
src/GitHub/GitHubClient.test.ts src/GitHub/GitHubClient.test.ts
src/GitHub/GitHubClient.ts src/GitHub/GitHubClient.ts
src/GitHub/GitHubConnector.ts
src/GitHub/GitHubOAuthService.ts
src/Index.ts src/Index.ts
src/Juno/JunoClient.test.ts
src/Juno/JunoClient.ts
src/Platform/Hosted/Authorization.ts src/Platform/Hosted/Authorization.ts
src/ReactDevTools.ts src/ReactDevTools.ts
src/Shared/Constants.ts src/Shared/Constants.ts
@@ -143,20 +130,13 @@ src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
src/Explorer/Controls/TreeComponent/TreeComponent.tsx src/Explorer/Controls/TreeComponent/TreeComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx
src/Explorer/Notebook/NotebookComponent/contents/index.tsx src/Explorer/Notebook/NotebookComponent/contents/index.tsx
src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx
src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx

View File

@@ -39,7 +39,6 @@ module.exports = {
"@typescript-eslint/switch-exhaustiveness-check": "error", "@typescript-eslint/switch-exhaustiveness-check": "error",
"@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-extraneous-class": "error",
"no-null/no-null": "error",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }], "prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
eqeqeq: "error", eqeqeq: "error",

View File

@@ -22,5 +22,6 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true, "source.fixAll.eslint": true,
"source.organizeImports": true "source.organizeImports": true
} },
"typescript.preferences.importModuleSpecifier": "non-relative"
} }

View File

@@ -37,8 +37,8 @@ module.exports = {
global: { global: {
branches: 25, branches: 25,
functions: 25, functions: 25,
lines: 29.5, lines: 29,
statements: 29.5, statements: 29,
}, },
}, },
@@ -129,6 +129,8 @@ module.exports = {
// The test environment that will be used for testing // The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom", // testEnvironment: "jest-environment-jsdom",
modulePaths: ["node_modules", "<rootDir>/src"],
// Options that will be passed to the testEnvironment // Options that will be passed to the testEnvironment
// testEnvironmentOptions: {}, // testEnvironmentOptions: {},

View File

@@ -96,7 +96,7 @@ export class Flights {
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
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 NotebooksDownBanner = "notebooksdownbanner";
} }
export class AfecFeatures { export class AfecFeatures {
@@ -339,9 +339,16 @@ export enum ConflictOperationType {
} }
export enum ConnectionStatusType { export enum ConnectionStatusType {
Connect = "Connect",
Connecting = "Connecting", Connecting = "Connecting",
Connected = "Connected", Connected = "Connected",
Failed = "Connection Failed", Failed = "Connection Failed",
Reconnect = "Reconnect",
}
export enum ContainerStatusType {
Active = "Active",
Disconnected = "Disconnected",
} }
export const EmulatorMasterKey = export const EmulatorMasterKey =
@@ -353,15 +360,37 @@ 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 = 5000; 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 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 saveNotebookModalContent =
"This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends.";
public static newNotebookModalTitle = "Create notebook in temporary workspace";
public static newNotebookUploadModalTitle = "Upload notebook to temporary workspace";
public static newNotebookModalContent1 =
"A temporary workspace will be created to enable you to work with notebooks. When the session expires, any notebooks in the workspace will be removed.";
public static newNotebookModalContent2 =
"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 galleryNotebookDownloadContent1 =
"To download, run, and make changes to this sample notebook, a temporary workspace will be created. When the session expires, any notebooks in the workspace will be removed.";
public static galleryNotebookDownloadContent2 =
"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 cosmosNotebookHomePageUrl = "https://aka.ms/cosmos-notebooks-limits";
public static cosmosNotebookGitDocumentationUrl = "https://aka.ms/cosmos-notebooks-github";
public static learnMore = "Learn more.";
} }
export class SparkLibrary { export class SparkLibrary {

View File

@@ -58,6 +58,7 @@ let configContext: Readonly<ConfigContext> = {
allowedJunoOrigins: [ allowedJunoOrigins: [
"https://juno-test.documents-dev.windows-int.net", "https://juno-test.documents-dev.windows-int.net",
"https://juno-test2.documents-dev.windows-int.net", "https://juno-test2.documents-dev.windows-int.net",
"https://juno-test3.documents-dev.windows-int.net",
"https://tools.cosmos.azure.com", "https://tools.cosmos.azure.com",
"https://tools-staging.cosmos.azure.com", "https://tools-staging.cosmos.azure.com",
"https://localhost", "https://localhost",

View File

@@ -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,38 @@ 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 {
cosmosEndpoint: string;
}
export interface IAccountData {
subscriptionId: string;
resourceGroup: string;
dbAccountName: string;
}
export interface IContainerData extends IAccountData {
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 {

View File

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

View File

@@ -30,6 +30,7 @@ export interface DialogState {
onOk: () => void, onOk: () => void,
cancelLabel: string, cancelLabel: string,
onCancel: () => void, onCancel: () => void,
contentHtml?: JSX.Element,
choiceGroupProps?: IChoiceGroupProps, choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps, textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean primaryButtonDisabled?: boolean
@@ -58,6 +59,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
onOk: () => void, onOk: () => void,
cancelLabel: string, cancelLabel: string,
onCancel: () => void, onCancel: () => void,
contentHtml?: JSX.Element,
choiceGroupProps?: IChoiceGroupProps, choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps, textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean primaryButtonDisabled?: boolean
@@ -76,6 +78,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
get().closeDialog(); get().closeDialog();
onCancel && onCancel(); onCancel && onCancel();
}, },
contentHtml,
choiceGroupProps, choiceGroupProps,
textFieldProps, textFieldProps,
primaryButtonDisabled, primaryButtonDisabled,
@@ -124,6 +127,7 @@ export interface DialogProps {
type?: DialogType; type?: DialogType;
showCloseButton?: boolean; showCloseButton?: boolean;
onDismiss?: () => void; onDismiss?: () => void;
contentHtml?: JSX.Element;
} }
const DIALOG_MIN_WIDTH = "400px"; const DIALOG_MIN_WIDTH = "400px";
@@ -150,6 +154,7 @@ export const Dialog: FC = () => {
type, type,
showCloseButton, showCloseButton,
onDismiss, onDismiss,
contentHtml,
} = props || {}; } = props || {};
const dialogProps: IDialogProps = { const dialogProps: IDialogProps = {
@@ -191,6 +196,7 @@ export const Dialog: FC = () => {
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" /> {linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link> </Link>
)} )}
{contentHtml}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />} {progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter> <DialogFooter>
<PrimaryButton {...primaryButtonProps} /> <PrimaryButton {...primaryButtonProps} />

View File

@@ -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(

View File

@@ -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);
}, },
}; };

View File

@@ -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", () => {

View File

@@ -17,6 +17,7 @@ import Explorer from "../../Explorer";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2"; import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper"; import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer"; import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { useNotebook } from "../../Notebook/useNotebook";
import { Dialog, TextFieldProps, useDialog } from "../Dialog"; import { Dialog, TextFieldProps, useDialog } from "../Dialog";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent"; import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less"; import "./NotebookViewerComponent.less";
@@ -51,7 +52,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,
@@ -146,7 +147,7 @@ export class NotebookViewerComponent
<NotebookMetadataComponent <NotebookMetadataComponent
data={this.state.galleryItem} data={this.state.galleryItem}
isFavorite={this.state.isFavorite} isFavorite={this.state.isFavorite}
downloadButtonText={this.props.container && "Download to my notebooks"} downloadButtonText={this.props.container && `Download to ${useNotebook.getState().notebookFolderName}`}
onTagClick={this.props.onTagClick} onTagClick={this.props.onTagClick}
onFavoriteClick={this.favoriteItem} onFavoriteClick={this.favoriteItem}
onUnfavoriteClick={this.unfavoriteItem} onUnfavoriteClick={this.unfavoriteItem}

View File

@@ -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,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return true; return true;
}, },
}; };
if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) {
this.calculateTotalThroughputUsed();
}
} }
componentDidMount(): void { componentDidMount(): void {
@@ -254,6 +262,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 +493,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 +675,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 - 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 - this.totalThroughputUsed < newThroughput - this.offer.manualThroughput) {
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 +946,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) {

View File

@@ -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}
/> />
); );

View File

@@ -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

View File

@@ -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],

View File

@@ -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", () => {

View File

@@ -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 - 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 - 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);
} }
}; };

View File

@@ -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}
> >

View File

@@ -1,16 +1,27 @@
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 { 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,
IAccountData,
IContainerData,
IPhoenixConnectionInfoResult,
IProvisionData,
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";
@@ -26,7 +37,6 @@ import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { import {
get as getWorkspace, get as getWorkspace,
listByDatabaseAccount, listByDatabaseAccount,
listConnectionInfo,
start, start,
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
@@ -162,24 +172,9 @@ export default class Explorer {
); );
useNotebook.subscribe( useNotebook.subscribe(
async () => { async () => this.initiateAndRefreshNotebookList(),
if (!this.notebookManager) { (state) => [state.isNotebookEnabled, state.isRefreshed],
const NotebookManager = await ( shallow
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
).default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
},
(state) => state.isNotebookEnabled
); );
this.resourceTree = new ResourceTreeAdapter(this); this.resourceTree = new ResourceTreeAdapter(this);
@@ -189,6 +184,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,
}); });
} }
@@ -212,6 +208,23 @@ export default class Explorer {
this.refreshExplorer(); this.refreshExplorer();
} }
public async initiateAndRefreshNotebookList(): Promise<void> {
if (!this.notebookManager) {
const NotebookManager = (await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"))
.default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
}
public openEnableSynapseLinkDialog(): void { public openEnableSynapseLinkDialog(): void {
const addSynapseLinkDialogProps: DialogProps = { const addSynapseLinkDialogProps: DialogProps = {
linkProps: { linkProps: {
@@ -340,49 +353,80 @@ export default class Explorer {
if (!databaseAccount) { if (!databaseAccount) {
throw new Error("No database account specified"); throw new Error("No database account specified");
} }
if (this._isInitializingNotebooks) { if (this._isInitializingNotebooks) {
return; return;
} }
this._isInitializingNotebooks = true; this._isInitializingNotebooks = true;
if (userContext.features.phoenix) {
const provisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
resourceId: userContext.databaseAccount.id,
dbAccountName: userContext.databaseAccount.name,
aadToken: userContext.authorizationToken,
resourceGroup: userContext.resourceGroup,
subscriptionId: userContext.subscriptionId,
};
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
if (connectionInfo.data && connectionInfo.data.notebookServerUrl) {
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
});
}
} else {
await this.ensureNotebookWorkspaceRunning();
const connectionInfo = await listConnectionInfo(
userContext.subscriptionId,
userContext.resourceGroup,
databaseAccount.name,
"default"
);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
});
}
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
this.refreshNotebookList(); this.refreshNotebookList();
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
} }
public async allocateContainer(): Promise<void> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const isAllocating = useNotebook.getState().isAllocating;
if (
isAllocating === false &&
(notebookServerInfo === undefined ||
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
};
const accountData: IAccountData = {
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
};
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
try {
useNotebook.getState().setIsAllocating(true);
const connectionInfo = await this.phoenixClient.allocateContainer(provisionData, accountData);
await this.setNotebookInfo(connectionInfo, connectionStatus);
} catch (error) {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetContainerConnection(connectionStatus);
throw error;
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
}
private async setNotebookInfo(
connectionInfo: IResponse<IPhoenixConnectionInfoResult>,
connectionStatus: DataModels.ContainerConnectionInfo
) {
if (connectionInfo.status === HttpStatusCodes.OK && connectionInfo.data && connectionInfo.data.notebookServerUrl) {
const containerData: IContainerData = {
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
forwardingId: connectionInfo.data.forwardingId,
};
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));
} else {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetContainerConnection(connectionStatus);
}
useNotebook.getState().setIsAllocating(false);
}
public resetNotebookWorkspace(): void { public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError( handleError(
@@ -391,11 +435,10 @@ export default class Explorer {
); );
return; return;
} }
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: "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?",
primaryButtonText: "OK", primaryButtonText: "OK",
secondaryButtonText: "Cancel", secondaryButtonText: "Cancel",
onPrimaryButtonClick: this._resetNotebookWorkspace, onPrimaryButtonClick: this._resetNotebookWorkspace,
@@ -453,16 +496,48 @@ 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;
logConsoleInfo("Successfully reset notebook workspace"); if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace); const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
logConsoleError(error);
return;
}
useTabs.getState().closeAllNotebookTabs(true);
connectionStatus = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
if (connectionInfo && connectionInfo.status && connectionInfo.status === HttpStatusCodes.OK) {
if (connectionInfo.data && connectionInfo.data.notebookServerUrl) {
await this.setNotebookInfo(connectionInfo, connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
logConsoleInfo("Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
} else {
logConsoleError(`Failed to reset notebook workspace`);
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace);
connectionStatus = {
status: ConnectionStatusType.Reconnect,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
} 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.ResetNotebookWorkspace, {
error: getErrorMessage(error), error: getErrorMessage(error),
errorStack: getErrorStack(error), errorStack: getErrorStack(error),
}); });
connectionStatus = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
throw error; throw error;
} finally { } finally {
clearInProgressMessage(); clearInProgressMessage();
@@ -654,6 +729,9 @@ export default class Explorer {
if (!notebookContentItem || !notebookContentItem.path) { if (!notebookContentItem || !notebookContentItem.path) {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
} }
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenix) {
await this.allocateContainer();
}
const notebookTabs = useTabs const notebookTabs = useTabs
.getState() .getState()
@@ -875,9 +953,51 @@ export default class Explorer {
handleError(error, "Explorer/onNewNotebookClicked"); handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error); throw new Error(error);
} }
const isPhoenixEnabled = useNotebook.getState().isPhoenix;
if (isPhoenixEnabled) {
if (isGithubTree) {
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
};
} else {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookModalTitle,
undefined,
"Create",
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
},
"Cancel",
undefined,
this.getNewNoteWarningText()
);
}
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
}
}
parent = parent || this.resourceTree.myNotebooksContentRoot; private getNewNoteWarningText(): JSX.Element {
return (
<>
<p>{Notebook.newNotebookModalContent1}</p>
<br />
<p>
{Notebook.newNotebookModalContent2}
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
@@ -924,7 +1044,26 @@ export default class Explorer {
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item); await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
} }
public openNotebookTerminal(kind: ViewModels.TerminalKind): void { public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (useNotebook.getState().isPhoenix) {
await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
this.connectToNotebookTerminal(kind);
} else {
useDialog
.getState()
.showOkModalDialog(
"Failed to connect",
"Failed to connect to temporary workspace. This could happen because of network issues. Please refresh the page and try again."
);
}
} else {
this.connectToNotebookTerminal(kind);
}
}
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
let title: string; let title: string;
switch (kind) { switch (kind) {
@@ -975,7 +1114,7 @@ export default class Explorer {
notebookUrl?: string, notebookUrl?: string,
galleryItem?: IGalleryItem, galleryItem?: IGalleryItem,
isFavorite?: boolean isFavorite?: boolean
) { ): Promise<void> {
const title = "Gallery"; const title = "Gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default; const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
const galleryTab = useTabs const galleryTab = useTabs
@@ -1018,7 +1157,9 @@ export default class Explorer {
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} /> <CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
); );
} else { } else {
await useDatabases.getState().loadDatabaseOffers(); userContext.databaseAccount?.properties.capacity?.totalThroughputLimit
? 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} />);
@@ -1044,10 +1185,9 @@ export default class Explorer {
} }
public async handleOpenFileAction(path: string): Promise<void> { public async handleOpenFileAction(path: string): Promise<void> {
if ( if (useNotebook.getState().isPhoenix) {
userContext.features.phoenix === false && await this.allocateContainer();
!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) } else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
) {
this._openSetupNotebooksPaneForQuickstart(); this._openSetupNotebooksPaneForQuickstart();
} }
@@ -1079,7 +1219,24 @@ export default class Explorer {
} }
public openUploadFilePanel(parent?: NotebookContentItem): void { public openUploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot; if (useNotebook.getState().isPhoenix) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
"Upload",
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
},
"Cancel",
undefined,
this.getNewNoteWarningText()
);
}
}
private uploadFilePanel(parent?: NotebookContentItem): void {
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
@@ -1088,27 +1245,40 @@ export default class Explorer {
); );
} }
public getDownloadModalConent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenix) {
return (
<>
<p>{Notebook.galleryNotebookDownloadContent1}</p>
<br />
<p>
{Notebook.galleryNotebookDownloadContent2}
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
}
public async refreshExplorer(): Promise<void> { public async refreshExplorer(): Promise<void> {
userContext.authType === AuthType.ResourceToken userContext.authType === AuthType.ResourceToken
? 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 || useNotebook.getState().isPhoenix;
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(useNotebook.getState().isPhoenix && isPublicInternetAccessAllowed());
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled, isNotebookEnabled,
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
}); });
if (useNotebook.getState().isPhoenix) {
if (!userContext.features.notebooksTemporarilyDown) {
if (isNotebookEnabled) { if (isNotebookEnabled) {
await this.initNotebooks(userContext.databaseAccount); await this.initNotebooks(userContext.databaseAccount);
} else if (this.notebookToImport) { } else if (this.notebookToImport) {

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as Q from "q"; import * as Q from "q";
import * as React from "react"; import * as React from "react";
@@ -294,8 +296,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.setGremlinParams(); this.setGremlinParams();
} }
const selectedNode = this.state.highlightedNode;
props.onGraphAccessorCreated({ props.onGraphAccessorCreated({
applyFilter: this.submitQuery.bind(this), applyFilter: this.submitQuery.bind(this),
addVertex: this.addVertex.bind(this), addVertex: this.addVertex.bind(this),
@@ -303,7 +303,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}); });
} // constructor } // constructor
public shareIGraphConfig(igraphConfig: IGraphConfig) { public shareIGraphConfig(igraphConfig: IGraphConfig): void {
this.setState({ this.setState({
igraphConfig: { ...igraphConfig }, igraphConfig: { ...igraphConfig },
}); });
@@ -330,10 +330,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const partitionKeyProperty = this.props.collectionPartitionKeyProperty; const partitionKeyProperty = this.props.collectionPartitionKeyProperty;
// aggregate all the properties, remove dropped ones // aggregate all the properties, remove dropped ones
let finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties); const finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
// Compose the query // Compose the query
let pkId = editedProperties.pkId; const pkId = editedProperties.pkId;
let updateQueryFragment = ""; let updateQueryFragment = "";
finalProperties.forEach((p) => { finalProperties.forEach((p) => {
@@ -422,7 +422,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* Called from ko binding * Called from ko binding
* @param id * @param id
*/ */
public selectNode(id: string) { public selectNode(id: string): void {
if (!this.d3ForceGraph) { if (!this.d3ForceGraph) {
console.warn("Attempting to select node, but d3ForceGraph not initialized, yet."); console.warn("Attempting to select node, but d3ForceGraph not initialized, yet.");
return; return;
@@ -431,7 +431,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.d3ForceGraph.selectNode(id); this.d3ForceGraph.selectNode(id);
} }
public deleteHighlightedNode() { public deleteHighlightedNode(): void {
if (!this.state.highlightedNode) { if (!this.state.highlightedNode) {
GraphExplorer.reportToConsole(ConsoleDataType.Error, "No highlighted node to remove."); GraphExplorer.reportToConsole(ConsoleDataType.Error, "No highlighted node to remove.");
return; return;
@@ -467,23 +467,23 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* Is of type: {e: GremlinEdge, v: GremlinVertex}[] * Is of type: {e: GremlinEdge, v: GremlinVertex}[]
* @param data * @param data
*/ */
public static isEdgeVertexPairArray(data: any) { public static isEdgeVertexPairArray(data: any): boolean {
if (!(data instanceof Array)) { if (!(data instanceof Array)) {
GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result not an array", data); GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result not an array", data);
return false; return false;
} }
let pairs: any[] = data; const pairs: any[] = data;
for (let i = 0; i < pairs.length; i++) { for (let i = 0; i < pairs.length; i++) {
const item = pairs[i]; const item = pairs[i];
if ( if (
!item.hasOwnProperty("e") || !Object.prototype.hasOwnProperty.call(item, "e") ||
!item.hasOwnProperty("v") || !Object.prototype.hasOwnProperty.call(item, "v") ||
!item["e"].hasOwnProperty("id") || !Object.prototype.hasOwnProperty.call(item["e"], "id") ||
!item["e"].hasOwnProperty("type") || !Object.prototype.hasOwnProperty.call(item["e"], "type") ||
item["e"].type !== "edge" || item["e"].type !== "edge" ||
!item["v"].hasOwnProperty("id") || !Object.prototype.hasOwnProperty.call(item["v"], "id") ||
!item["v"].hasOwnProperty("type") || !Object.prototype.hasOwnProperty.call(item["e"], "type") ||
item["v"].type !== "vertex" item["v"].type !== "vertex"
) { ) {
return false; return false;
@@ -514,7 +514,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Try hitting cache first // Try hitting cache first
const cache = outE ? this.outECache : this.inECache; const cache = outE ? this.outECache : this.inECache;
const pairs = cache.retrieve(vertex.id, startIndex, pageSize); const pairs = cache.retrieve(vertex.id, startIndex, pageSize);
if (pairs != null && pairs.length === pageSize) { if (pairs !== null && pairs.length === pageSize) {
const msg = `Retrieved ${pairs.length} ${outE ? "outE" : "inE"} edges from cache for vertex id: ${vertex.id}`; const msg = `Retrieved ${pairs.length} ${outE ? "outE" : "inE"} edges from cache for vertex id: ${vertex.id}`;
GraphExplorer.reportToConsole(ConsoleDataType.Info, msg); GraphExplorer.reportToConsole(ConsoleDataType.Info, msg);
return Q.resolve(pairs); return Q.resolve(pairs);
@@ -588,7 +588,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
vertex._outEAllLoaded && vertex._outEAllLoaded &&
vertex._inEAllLoaded vertex._inEAllLoaded
) { ) {
console.info("No more edges to load for vertex " + vertex.id);
updateGraphData(); updateGraphData();
return Q.resolve(graphData); return Q.resolve(graphData);
} }
@@ -668,7 +667,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
} }
); );
return promise.then((nbPairsFetched: number) => { return promise.then(() => {
if (offsetIndex >= GraphExplorer.LOAD_PAGE_SIZE || !vertex._outEAllLoaded || !vertex._inEAllLoaded) { if (offsetIndex >= GraphExplorer.LOAD_PAGE_SIZE || !vertex._outEAllLoaded || !vertex._inEAllLoaded) {
vertex._pagination = { vertex._pagination = {
total: total:
@@ -754,7 +753,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* Create a new edge in docdb and update graph * Create a new edge in docdb and update graph
* @param e * @param e
*/ */
public createNewEdge(e: GraphNewEdgeData): Q.Promise<any> { public createNewEdge(e: GraphNewEdgeData): Q.Promise<unknown> {
const q = `g.V('${GraphUtil.escapeSingleQuotes(e.inputOutV)}').addE('${GraphUtil.escapeSingleQuotes( const q = `g.V('${GraphUtil.escapeSingleQuotes(e.inputOutV)}').addE('${GraphUtil.escapeSingleQuotes(
e.label e.label
)}').To(g.V('${GraphUtil.escapeSingleQuotes(e.inputInV)}'))`; )}').To(g.V('${GraphUtil.escapeSingleQuotes(e.inputInV)}'))`;
@@ -772,8 +771,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
let edge = edges[0]; const edge = edges[0];
let graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.addEdge(edge); graphData.addEdge(edge);
// Allow loadNeighbors to load list new edge // Allow loadNeighbors to load list new edge
@@ -800,10 +799,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* Manually update in-memory graph. * Manually update in-memory graph.
* @param edgeId * @param edgeId
*/ */
public removeEdge(edgeId: string): Q.Promise<any> { public removeEdge(edgeId: string): Q.Promise<unknown> {
return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then( return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then(
() => { () => {
let graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.removeEdge(edgeId, false); graphData.removeEdge(edgeId, false);
this.updateGraphData(graphData, this.state.igraphConfig); this.updateGraphData(graphData, this.state.igraphConfig);
}, },
@@ -826,10 +825,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return false; return false;
} }
let vertices: any[] = data; const vertices: any[] = data;
if (vertices.length > 0) { if (vertices.length > 0) {
let v0 = vertices[0]; const v0 = vertices[0];
if (!v0.hasOwnProperty("id") || !v0.hasOwnProperty("type") || v0.type !== "vertex") { if (
!Object.prototype.hasOwnProperty.call(v0, "id") ||
!Object.prototype.hasOwnProperty.call(v0, "type") ||
v0.type !== "vertex"
) {
return false; return false;
} }
} }
@@ -837,7 +840,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
} }
public processGremlinQueryResults(result: GremlinClient.GremlinRequestResult): void { public processGremlinQueryResults(result: GremlinClient.GremlinRequestResult): void {
const data = result.data as any; const data = result.data as GraphData.GremlinVertex[];
this.setFilterQueryStatus(FilterQueryStatus.GraphEmptyResult); this.setFilterQueryStatus(FilterQueryStatus.GraphEmptyResult);
if (data === null) { if (data === null) {
@@ -927,13 +930,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
throw { title: err }; throw { title: err };
} }
if (vertices == null || vertices.length < 1) { if (vertices === null || vertices.length < 1) {
const err = "Failed to create vertex (no vertex in response)"; const err = "Failed to create vertex (no vertex in response)";
GraphExplorer.reportToConsole(ConsoleDataType.Error, err, vertices); GraphExplorer.reportToConsole(ConsoleDataType.Error, err, vertices);
throw { title: err }; throw { title: err };
} }
let vertex = vertices[0]; const vertex = vertices[0];
const graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.addVertex(vertex); graphData.addVertex(vertex);
this.updateGraphData(graphData, this.state.igraphConfig); this.updateGraphData(graphData, this.state.igraphConfig);
@@ -1022,7 +1025,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.gremlinClient.destroy(); this.gremlinClient.destroy();
} }
public componentDidMount(): void { public componentDidMount(): void {
if (this.props.onLoadStartKey != null && this.props.onLoadStartKey != undefined) { if (this.props.onLoadStartKey !== null && this.props.onLoadStartKey !== undefined) {
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.Tab, Action.Tab,
{ {
@@ -1082,7 +1085,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void; public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void; public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) { public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
let errorDataStr: string = ""; let errorDataStr = "";
if (errorData && errorData.length > 0) { if (errorData && errorData.length > 0) {
console.error(msg, errorData); console.error(msg, errorData);
errorDataStr = ": " + JSON.stringify(errorData); errorDataStr = ": " + JSON.stringify(errorData);
@@ -1161,12 +1164,15 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
)}"` )}"`
).then( ).then(
(documents: DataModels.DocumentId[]) => { (documents: DataModels.DocumentId[]) => {
$.each(documents, (index: number, doc: any) => { $.each(
newIconsMap[doc["_graph_icon_property_value"]] = { documents,
data: doc["icon"], (index: number, doc: { _graph_icon_property_value: string; icon: string; format: string }) => {
format: doc["format"], newIconsMap[doc["_graph_icon_property_value"]] = {
}; data: doc["icon"],
}); format: doc["format"],
};
}
);
// Update graph configuration // Update graph configuration
this.setState({ this.setState({
@@ -1223,8 +1229,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const key = this.state.igraphConfig.nodeCaption; const key = this.state.igraphConfig.nodeCaption;
return $.map( return $.map(
this.state.rootMap, this.state.rootMap,
(value: any, index: number): LeftPane.CaptionId => { (value: any): LeftPane.CaptionId => {
let result = GraphData.GraphData.getNodePropValue(value, key); const result = GraphData.GraphData.getNodePropValue(value, key);
return { return {
caption: result !== undefined ? result : value.id, caption: result !== undefined ? result : value.id,
id: value.id, id: value.id,
@@ -1237,7 +1243,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* Selecting a root node means * Selecting a root node means
* @param node * @param node
*/ */
private selectRootNode(id: string): Q.Promise<any> { private selectRootNode(id: string): Q.Promise<unknown> {
if (!this.d3ForceGraph) { if (!this.d3ForceGraph) {
console.warn("Attempting to reset zoom, but d3ForceGraph not initialized, yet."); console.warn("Attempting to reset zoom, but d3ForceGraph not initialized, yet.");
} else { } else {
@@ -1282,7 +1288,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.collectNodeProperties(this.originalGraphData.vertices); this.collectNodeProperties(this.originalGraphData.vertices);
this.updatePropertiesPane(id); this.updatePropertiesPane(id);
}, },
(reason: any) => { (reason: string) => {
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to select root node. Reason:${reason}`); GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to select root node. Reason:${reason}`);
} }
); );
@@ -1349,10 +1355,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private getPkIdFromVertex(v: GraphData.GremlinVertex): string { private getPkIdFromVertex(v: GraphData.GremlinVertex): string {
if ( if (
this.props.collectionPartitionKeyProperty && this.props.collectionPartitionKeyProperty &&
v.hasOwnProperty("properties") && Object.prototype.hasOwnProperty.call(v, "properties") &&
v.properties.hasOwnProperty(this.props.collectionPartitionKeyProperty) && Object.prototype.hasOwnProperty.call(v.properties, this.props.collectionPartitionKeyProperty) &&
v.properties[this.props.collectionPartitionKeyProperty].length > 0 && v.properties[this.props.collectionPartitionKeyProperty].length > 0 &&
v.properties[this.props.collectionPartitionKeyProperty][0].hasOwnProperty("value") Object.prototype.hasOwnProperty.call(v.properties[this.props.collectionPartitionKeyProperty][0], "value")
) { ) {
const pk = v.properties[this.props.collectionPartitionKeyProperty][0].value; const pk = v.properties[this.props.collectionPartitionKeyProperty][0].value;
return GraphExplorer.generatePkIdPair(pk, v.id); return GraphExplorer.generatePkIdPair(pk, v.id);
@@ -1370,8 +1376,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private getPkIdFromNodeData(v: GraphHighlightedNodeData): string { private getPkIdFromNodeData(v: GraphHighlightedNodeData): string {
if ( if (
this.props.collectionPartitionKeyProperty && this.props.collectionPartitionKeyProperty &&
v.hasOwnProperty("properties") && Object.prototype.hasOwnProperty.call(v, "properties") &&
v.properties.hasOwnProperty(this.props.collectionPartitionKeyProperty) Object.prototype.hasOwnProperty.call(v.properties, this.props.collectionPartitionKeyProperty)
) { ) {
const pk = v.properties[this.props.collectionPartitionKeyProperty]; const pk = v.properties[this.props.collectionPartitionKeyProperty];
return GraphExplorer.generatePkIdPair(pk[0] as PartitionKeyValueType, v.id); return GraphExplorer.generatePkIdPair(pk[0] as PartitionKeyValueType, v.id);
@@ -1388,14 +1394,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @return id * @return id
*/ */
public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string { public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string {
let { id } = d; const { id } = d;
if (typeof id !== "string") { if (typeof id !== "string") {
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`; const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
logConsoleError(error); logConsoleError(error);
throw new Error(error); throw new Error(error);
} }
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) { if (collectionPartitionKeyProperty && Object.prototype.hasOwnProperty.call(d, collectionPartitionKeyProperty)) {
let pk = (d as any)[collectionPartitionKeyProperty]; let pk = (d as any)[collectionPartitionKeyProperty];
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") { if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
if (Array.isArray(pk) && pk.length > 0) { if (Array.isArray(pk) && pk.length > 0) {
@@ -1425,7 +1431,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`; }"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
return this.executeNonPagedDocDbQuery(q).then( return this.executeNonPagedDocDbQuery(q).then(
(documents: DataModels.DocumentId[]) => { (documents: DataModels.DocumentId[]) => {
let possibleVertices = [] as PossibleVertex[]; const possibleVertices = [] as PossibleVertex[];
$.each(documents, (index: number, item: any) => { $.each(documents, (index: number, item: any) => {
if (highlightedNodeId && item.id === highlightedNodeId) { if (highlightedNodeId && item.id === highlightedNodeId) {
// Exclude highlighed node in the list // Exclude highlighed node in the list
@@ -1439,7 +1445,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
caption: item.p, caption: item.p,
}); });
} else { } else {
if (item.hasOwnProperty("p")) { if (Object.prototype.hasOwnProperty.call(item, "p")) {
possibleVertices.push({ possibleVertices.push({
value: item.id, value: item.id,
caption: item.p[0]["_value"], caption: item.p[0]["_value"],
@@ -1462,17 +1468,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @param addedEdges * @param addedEdges
* @return promise when done * @return promise when done
*/ */
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<any> { private editGraphEdges(editedEdges: EditedEdges): Q.Promise<unknown> {
let promises = []; const promises = [];
// Drop edges // Drop edges
for (let i = 0; i < editedEdges.droppedIds.length; i++) { for (let i = 0; i < editedEdges.droppedIds.length; i++) {
let id = editedEdges.droppedIds[i]; const id = editedEdges.droppedIds[i];
promises.push(this.removeEdge(id)); promises.push(this.removeEdge(id));
} }
// Add edges // Add edges
for (let i = 0; i < editedEdges.addedEdges.length; i++) { for (let i = 0; i < editedEdges.addedEdges.length; i++) {
let e = editedEdges.addedEdges[i]; const e = editedEdges.addedEdges[i];
promises.push( promises.push(
this.createNewEdge(e).then(() => { this.createNewEdge(e).then(() => {
// Reload neighbors in case we linked to a vertex that isn't loaded in the graph // Reload neighbors in case we linked to a vertex that isn't loaded in the graph
@@ -1525,7 +1531,9 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* For unit testing purposes * For unit testing purposes
*/ */
public onGraphUpdated(timestamp: number): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public onGraphUpdated(_timestamp: number): void {}
/** /**
* Get node properties for styling purposes. Result is the union of all properties of all nodes. * Get node properties for styling purposes. Result is the union of all properties of all nodes.
@@ -1533,17 +1541,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private collectNodeProperties(vertices: GraphData.GremlinVertex[]) { private collectNodeProperties(vertices: GraphData.GremlinVertex[]) {
const props = {} as any; // Hashset const props = {} as any; // Hashset
$.each(vertices, (index: number, item: GraphData.GremlinVertex) => { $.each(vertices, (index: number, item: GraphData.GremlinVertex) => {
for (var p in item) { for (const p in item) {
// DocDB: Exclude type because it's always 'vertex' // DocDB: Exclude type because it's always 'vertex'
if (p !== "type" && typeof (item as any)[p] === "string") { if (p !== "type" && typeof (item as any)[p] === "string") {
props[p] = true; props[p] = true;
} }
} }
// Inspect properties // Inspect properties
if (item.hasOwnProperty("properties")) { if (Object.prototype.hasOwnProperty.call(item, "properties")) {
// TODO This is DocDB-graph specific // TODO This is DocDB-graph specific
// Assume each property value is [{value:... }] // Assume each property value is [{value:... }]
for (var f in item.properties) { for (const f in item.properties) {
props[f] = true; props[f] = true;
} }
} }
@@ -1570,21 +1578,21 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
let data = this.originalGraphData.getVertexById(id); const data = this.originalGraphData.getVertexById(id);
// A bit of translation to make it easier to display // A bit of translation to make it easier to display
let props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {}; const props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
for (let p in data.properties) { for (const p in data.properties) {
props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value); props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value);
} }
// update neighbors // update neighbors
let sources: NeighborVertexBasicInfo[] = []; const sources: NeighborVertexBasicInfo[] = [];
let targets: NeighborVertexBasicInfo[] = []; const targets: NeighborVertexBasicInfo[] = [];
this.props.onResetDefaultGraphConfigValues(); this.props.onResetDefaultGraphConfigValues();
let nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice; const nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets); this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
let sData: GraphHighlightedNodeData = { const sData: GraphHighlightedNodeData = {
id: data.id, id: data.id,
label: data.label, label: data.label,
properties: props, properties: props,
@@ -1611,16 +1619,16 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
targets: NeighborVertexBasicInfo[] targets: NeighborVertexBasicInfo[]
): void { ): void {
// update neighbors // update neighbors
let gd = this.originalGraphData; const gd = this.originalGraphData;
let v = gd.getVertexById(id); const v = gd.getVertexById(id);
// Clear the array while keeping the references // Clear the array while keeping the references
sources.length = 0; sources.length = 0;
targets.length = 0; targets.length = 0;
let possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset const possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
for (let p in v.inE) { for (const p in v.inE) {
possibleEdgeLabels[p] = true; possibleEdgeLabels[p] = true;
const edges = v.inE[p]; const edges = v.inE[p];
$.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => { $.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => {
@@ -1629,7 +1637,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet // If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
return; return;
} }
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string; const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
sources.push({ sources.push({
name: caption, name: caption,
id: neighborId, id: neighborId,
@@ -1639,7 +1647,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}); });
} }
for (let p in v.outE) { for (const p in v.outE) {
possibleEdgeLabels[p] = true; possibleEdgeLabels[p] = true;
const edges = v.outE[p]; const edges = v.outE[p];
$.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => { $.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => {
@@ -1648,7 +1656,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet // If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
return; return;
} }
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string; const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
targets.push({ targets.push({
name: caption, name: caption,
id: neighborId, id: neighborId,
@@ -1660,7 +1668,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.setState({ this.setState({
possibleEdgeLabels: Object.keys(possibleEdgeLabels).map( possibleEdgeLabels: Object.keys(possibleEdgeLabels).map(
(value: string, index: number, array: string[]): InputTypeaheadComponent.Item => { (value: string): InputTypeaheadComponent.Item => {
return { caption: value, value: value }; return { caption: value, value: value };
} }
), ),
@@ -1681,20 +1689,20 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
let updatedVertex = vertices[0]; const updatedVertex = vertices[0];
if (this.originalGraphData.hasVertexId(updatedVertex.id)) { if (this.originalGraphData.hasVertexId(updatedVertex.id)) {
let currentVertex = this.originalGraphData.getVertexById(updatedVertex.id); const currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
// Copy updated properties // Copy updated properties
if (currentVertex.hasOwnProperty("properties")) { if (Object.prototype.hasOwnProperty.call(currentVertex, "properties")) {
delete currentVertex["properties"]; delete currentVertex["properties"];
} }
for (var p in updatedVertex) { for (const p in updatedVertex) {
(currentVertex as any)[p] = updatedVertex[p]; (currentVertex as any)[p] = updatedVertex[p];
} }
} }
// TODO This kind of assumes saveVertexProperty is done from property panes. // TODO This kind of assumes saveVertexProperty is done from property panes.
let hn = this.state.highlightedNode; const hn = this.state.highlightedNode;
if (hn && hn.id === updatedVertex.id) { if (hn && hn.id === updatedVertex.id) {
this.updatePropertiesPane(hn.id); this.updatePropertiesPane(hn.id);
} }
@@ -1708,7 +1716,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
igraphConfig?: IGraphConfig igraphConfig?: IGraphConfig
) { ) {
this.originalGraphData = graphData; this.originalGraphData = graphData;
let gd = JSON.parse(JSON.stringify(this.originalGraphData)); const gd = JSON.parse(JSON.stringify(this.originalGraphData));
if (!this.d3ForceGraph) { if (!this.d3ForceGraph) {
console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet."); console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet.");
return; return;
@@ -1873,7 +1881,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
promise promise
.then((result: GremlinClient.GremlinRequestResult) => this.processGremlinQueryResults(result)) .then((result: GremlinClient.GremlinRequestResult) => this.processGremlinQueryResults(result))
.catch((error: any) => { .catch((error: Error) => {
const errorMsg = `Failed to process query result: ${getErrorMessage(error)}`; const errorMsg = `Failed to process query result: ${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({ this.setState({

View File

@@ -58,7 +58,7 @@ export class LeftPaneComponent extends React.Component<LeftPaneComponentProps> {
className={className} className={className}
as="tr" as="tr"
aria-label={node.caption} aria-label={node.caption}
onActivated={(e) => this.props.onRootNodeSelected(node.id)} onActivated={() => this.props.onRootNodeSelected(node.id)}
key={node.id} key={node.id}
> >
<td className="resultItem"> <td className="resultItem">

View File

@@ -1,8 +1,8 @@
import React from "react";
import { mount, ReactWrapper } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
import * as Q from "q"; import * as Q from "q";
import { NodePropertiesComponent, NodePropertiesComponentProps, Mode } from "./NodePropertiesComponent"; import React from "react";
import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer"; import { GraphHighlightedNodeData, PossibleVertex } from "./GraphExplorer";
import { Mode, NodePropertiesComponent, NodePropertiesComponentProps } from "./NodePropertiesComponent";
describe("Property pane", () => { describe("Property pane", () => {
const title = "My Title"; const title = "My Title";
@@ -37,17 +37,18 @@ describe("Property pane", () => {
return { return {
expandedTitle: title, expandedTitle: title,
isCollapsed: false, isCollapsed: false,
onCollapsedChanged: (newValue: boolean): void => {}, onCollapsedChanged: jest.fn(),
node: highlightedNode, node: highlightedNode,
getPkIdFromNodeData: (v: GraphHighlightedNodeData): string => null, getPkIdFromNodeData: (): string => undefined,
collectionPartitionKeyProperty: null, collectionPartitionKeyProperty: undefined,
updateVertexProperties: (editedProperties: EditedProperties): Q.Promise<void> => Q.resolve(), updateVertexProperties: (): Q.Promise<void> => Q.resolve(),
selectNode: (id: string): void => {}, selectNode: jest.fn(),
updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(null), updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(undefined),
possibleEdgeLabels: null, possibleEdgeLabels: undefined,
editGraphEdges: (editedEdges: EditedEdges): Q.Promise<any> => Q.resolve(), //eslint-disable-next-line
deleteHighlightedNode: (): void => {}, editGraphEdges: (): Q.Promise<any> => Q.resolve(),
onModeChanged: (newMode: Mode): void => {}, deleteHighlightedNode: jest.fn(),
onModeChanged: jest.fn(),
viewMode: Mode.READONLY_PROP, viewMode: Mode.READONLY_PROP,
}; };
}; };

View File

@@ -72,7 +72,7 @@ export class NodePropertiesComponent extends React.Component<
super(props); super(props);
this.state = { this.state = {
editedProperties: { editedProperties: {
pkId: null, pkId: undefined,
readOnlyProperties: [], readOnlyProperties: [],
existingProperties: [], existingProperties: [],
addedProperties: [], addedProperties: [],
@@ -98,15 +98,12 @@ export class NodePropertiesComponent extends React.Component<
}; };
} }
public static getDerivedStateFromProps( public static getDerivedStateFromProps(props: NodePropertiesComponentProps): Partial<NodePropertiesComponentState> {
props: NodePropertiesComponentProps,
state: NodePropertiesComponentState
): Partial<NodePropertiesComponentState> {
if (props.viewMode !== Mode.READONLY_PROP) { if (props.viewMode !== Mode.READONLY_PROP) {
return { isDeleteConfirm: false }; return { isDeleteConfirm: false };
} }
return null; return undefined;
} }
public render(): JSX.Element { public render(): JSX.Element {
@@ -138,10 +135,10 @@ export class NodePropertiesComponent extends React.Component<
* @param value * @param value
*/ */
private static getTypeOption(value: any): ViewModels.InputPropertyValueTypeString { private static getTypeOption(value: any): ViewModels.InputPropertyValueTypeString {
if (value == null) { if (value === undefined) {
return "null"; return "null";
} }
let type = typeof value; const type = typeof value;
switch (type) { switch (type) {
case "number": case "number":
case "boolean": case "boolean":
@@ -172,10 +169,9 @@ export class NodePropertiesComponent extends React.Component<
]; ];
const existingProps: ViewModels.InputProperty[] = []; const existingProps: ViewModels.InputProperty[] = [];
if (this.props.node.hasOwnProperty("properties")) { if (this.props.node.hasOwnProperty("properties")) {
const hProps = this.props.node["properties"]; const hProps = this.props.node["properties"];
for (let p in hProps) { for (const p in hProps) {
const propValues = hProps[p]; const propValues = hProps[p];
(p === partitionKeyProperty ? readOnlyProps : existingProps).push({ (p === partitionKeyProperty ? readOnlyProps : existingProps).push({
key: p, key: p,
@@ -437,7 +433,7 @@ export class NodePropertiesComponent extends React.Component<
</div> </div>
); );
} else { } else {
return null; return undefined;
} }
} }

View File

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

View File

@@ -194,29 +194,26 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true); useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsPhoenix(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn).toBeDefined();
//TODO: modify once notebooks are available expect(openMongoShellBtn.disabled).toBe(false);
expect(openMongoShellBtn.disabled).toBe(true); expect(openMongoShellBtn.tooltipText).toBe("");
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available - button should be shown and enabled", () => { it("Notebooks is enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true); useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsPhoenix(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true); useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
//TODO: modify once notebooks are available expect(openMongoShellBtn.tooltipText).toBe("");
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => { it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
@@ -299,30 +296,27 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true); useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsPhoenix(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn).toBeDefined();
//TODO: modify once notebooks are available expect(openCassandraShellBtn.disabled).toBe(false);
expect(openCassandraShellBtn.disabled).toBe(true); expect(openCassandraShellBtn.tooltipText).toBe("");
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available - button should be shown and enabled", () => { it("Notebooks is enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true); useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsPhoenix(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true); useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
//TODO: modify once notebooks are available expect(openCassandraShellBtn.tooltipText).toBe("");
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
}); });
}); });

View File

@@ -10,7 +10,6 @@ import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg"; import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import GitHubIcon from "../../../../images/github.svg"; import GitHubIcon from "../../../../images/github.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg"; import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg"; import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg"; import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg"; import OpenInTabIcon from "../../../../images/open-in-tab.svg";
@@ -80,7 +79,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 (
@@ -98,7 +97,7 @@ export function createStaticCommandBarButtons(
} }
notebookButtons.forEach((btn) => { notebookButtons.forEach((btn) => {
if (userContext.features.notebooksTemporarilyDown) { if (useNotebook.getState().isPhoenix === false && userContext.features.notebooksDownBanner) {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) { if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg); applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) { } else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
@@ -109,11 +108,6 @@ export function createStaticCommandBarButtons(
} }
buttons.push(btn); buttons.push(btn);
}); });
} else {
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
buttons.push(createDivider());
buttons.push(createEnableNotebooksButton(container));
}
} }
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) { if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
@@ -168,7 +162,7 @@ export function createContextCommandBarButtons(
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) { if (useNotebook.getState().isShellEnabled) {
if (!userContext.features.notebooksTemporarilyDown) { if (useNotebook.getState().isPhoenix) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} }
} else { } else {
@@ -179,12 +173,12 @@ export function createContextCommandBarButtons(
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
tooltipText: tooltipText:
useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown useNotebook.getState().isShellEnabled && useNotebook.getState().isPhoenix === false
? Constants.Notebook.mongoShellTemporarilyDownMsg ? Constants.Notebook.mongoShellTemporarilyDownMsg
: undefined, : undefined,
disabled: disabled:
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") || (selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
(useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown), (useNotebook.getState().isShellEnabled && useNotebook.getState().isPhoenix === false),
}; };
buttons.push(newMongoShellBtn); buttons.push(newMongoShellBtn);
} }
@@ -310,8 +304,12 @@ 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} />), if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) {
await useDatabases.getState().loadAllOffers();
}
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />);
},
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
@@ -472,33 +470,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
}; };
} }
function createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
const label = "Enable Notebooks (Preview)";
const tooltip =
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const description =
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
return {
iconSrc: EnableNotebooksIcon,
iconAlt: label,
onCommandClick: () =>
useSidePanel
.getState()
.openSidePanel(
label,
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
),
commandButtonLabel: label,
hasPopup: false,
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
ariaLabel: label,
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
};
}
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps { function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal"; const label = "Open Terminal";
return { return {
@@ -596,7 +567,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
return { return {
iconSrc: GitHubIcon, iconSrc: GitHubIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => onCommandClick: () => {
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
@@ -606,7 +577,8 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
gitHubClientProp={container.notebookManager.gitHubClient} gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={junoClient} junoClientProp={junoClient}
/> />
), );
},
commandButtonLabel: label, commandButtonLabel: label,
hasPopup: false, hasPopup: false,
disabled: false, disabled: false,

View File

@@ -13,6 +13,7 @@ import { StyleConstants } from "../../../Common/Constants";
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";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { ConnectionStatus } from "./ConnectionStatusComponent"; import { ConnectionStatus } from "./ConnectionStatusComponent";
import { MemoryTracker } from "./MemoryTrackerComponent"; import { MemoryTracker } from "./MemoryTrackerComponent";
@@ -203,9 +204,9 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
}; };
}; };
export const createConnectionStatus = (key: string): ICommandBarItemProps => { export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
return { return {
key, key,
onRender: () => <ConnectionStatus />, onRender: () => <ConnectionStatus container={container} />,
}; };
}; };

View File

@@ -3,77 +3,182 @@
.connectionStatusContainer { .connectionStatusContainer {
cursor: default; cursor: default;
align-items: center; align-items: center;
margin: 0 9px;
border: 1px; border: 1px;
min-height: 44px; min-height: 44px;
> span { > span {
padding-right: 12px; padding-right: 12px;
font-size: 13px; font-size: 12px;
font-family: @DataExplorerFont; font-family: @DataExplorerFont;
color: @DefaultFontColor; color: @DefaultFontColor;
} }
&:focus{
outline: 0px;
}
} }
.connectionStatusFailed{ .commandReactBtn {
color: #bd1919; &:hover {
background-color: rgb(238, 247, 255);
color: rgb(32, 31, 30);
cursor: pointer;
}
&:focus{
outline: 1px dashed #605e5c;
}
} }
.ring-container { .connectedReactBtn {
&:hover {
background-color: rgb(238, 247, 255);
color: rgb(32, 31, 30);
cursor: pointer;
}
&:focus{
outline: 0px;
}
}
.connectIcon{
margin: 0px 4px;
height: 18px;
width: 18px;
color: rgb(0, 120, 212);
}
.status {
position: relative; position: relative;
} display: block;
margin-right: 8px;
.ringringGreen { width: 1em;
border: 3px solid green; height: 1em;
border-radius: 30px; font-size: 9px!important;
height: 18px; padding: 0px!important;
width: 18px; border-radius: 0.5em;
}
.status::before,
.status::after {
position: absolute; position: absolute;
margin: .4285em 0em 0em 0.07477em; content: "";
animation: pulsate 3s ease-out; }
animation-iteration-count: infinite;
opacity: 0.0 .status::before {
} top: 0;
.ringringYellow{ left: 0;
border: 3px solid #ffbf00; width: 1em;
border-radius: 30px; height: 1em;
height: 18px; background-color: rgba(#fff, 0.1);
width: 18px; border-radius: 100%;
position: absolute; opacity: 1;
margin: .4285em 0em 0em 0.07477em; transform: translate3d(0, 0, 0) scale(0);
animation: pulsate 3s ease-out; }
animation-iteration-count: infinite;
opacity: 0.0 .connected{
} background-color: green;
.ringringRed{ box-shadow:
border: 3px solid #bd1919; 0 0 0 0em rgba(green, 0),
border-radius: 30px; 0em 0.05em 0.1em rgba(#000000, 0.2);
height: 18px; transform: translate3d(0, 0, 0) scale(1);
width: 18px; }
position: absolute; .connecting{
margin: .4285em 0em 0em 0.07477em; background-color:#ffbf00;
animation: pulsate 3s ease-out; box-shadow:
animation-iteration-count: infinite; 0 0 0 0em rgba(#ffbf00, 0),
opacity: 0.0 0em 0.05em 0.1em rgba(#000000, 0.2);
} transform: translate3d(0, 0, 0) scale(1);
@keyframes pulsate { }
0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;} .failed{
15% {opacity: 0.8;} background-color:#bd1919;
25% {opacity: 0.6;} box-shadow:
45% {opacity: 0.4;} 0 0 0 0em rgba(#bd1919, 0),
70% {opacity: 0.3;} 0em 0.05em 0.1em rgba(#000000, 0.2);
100% {-webkit-transform: scale(.7, .7); opacity: 0.1;} transform: translate3d(0, 0, 0) scale(1);
} }
.locationGreenDot{
font-size: 20px; .status.connecting.is-animating {
margin-right: 0.07em; animation: status-outer-connecting 3000ms infinite;
color: green; }
} .status.failed.is-animating {
.locationYellowDot{ animation: status-outer-failed 3000ms infinite;
font-size: 20px; }
margin-right: 0.07em; .status.connected.is-animating {
color: #ffbf00; animation: status-outer-connected 3000ms infinite;
} }
.locationRedDot{ @keyframes status-outer-connected {
font-size: 20px;
margin-right: 0.07em; 0% {
color: #bd1919; transform: translate3d(0, 0, 0) scale(1);
} box-shadow: 0 0 0 0em #008000, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.6), 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@keyframes status-outer-failed {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #bd1919, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #c52d2d, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #b47b7b, 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@keyframes status-outer-connecting {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #ffbf00, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #f0dfad, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(198, 243, 198, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(213, 241, 213, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}

View File

@@ -1,17 +1,54 @@
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react"; import {
FocusTrapCallout,
FocusZone,
FocusZoneTabbableElements,
FontWeights,
Icon,
mergeStyleSets,
ProgressIndicator,
Stack,
Text,
TooltipHost,
} from "@fluentui/react";
import { useId } from "@fluentui/react-hooks";
import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button";
import * as React from "react"; import * as React from "react";
import { ConnectionStatusType } from "../../../Common/Constants"; import "../../../../less/hostedexplorer.less";
import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook"; import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less"; import "../CommandBar/ConnectionStatusComponent.less";
interface Props {
export const ConnectionStatus: React.FC = (): JSX.Element => { container: Explorer;
}
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
const connectionInfo = useNotebook((state) => state.connectionInfo);
const [second, setSecond] = React.useState("00"); const [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("locationYellowDot"); const [statusColor, setStatusColor] = React.useState("");
const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow"); const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
const toolTipContent = "Hosted runtime status."; 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;
@@ -31,6 +68,15 @@ export const ConnectionStatus: React.FC = (): JSX.Element => {
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);
@@ -38,35 +84,103 @@ export const ConnectionStatus: React.FC = (): JSX.Element => {
setMinute("00"); setMinute("00");
}; };
const connectionInfo = useNotebook((state) => state.connectionInfo); const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
if (!connectionInfo) { const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
return <></>; const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
if (
connectionInfo &&
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
) {
return (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
<span>{connectionInfo.status}</span>
</Stack>
</TooltipHost>
</ActionButton>
);
} }
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");
setToolTipContent("Connecting to temporary workspace.");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) { } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
stopTimer(); stopTimer();
setStatusColor("locationGreenDot"); setStatusColor("status connected is-animating");
setStatusColorAnimation("ringringGreen"); setToolTipContent("Connected to temporary workspace.");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) { } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
stopTimer(); stopTimer();
setStatusColor("locationRedDot"); setStatusColor("status failed is-animating");
setStatusColorAnimation("ringringRed"); setToolTipContent("Click here to Reconnect to temporary workspace.");
} }
return ( return (
<TooltipHost content={toolTipContent}> <>
<Stack className="connectionStatusContainer" horizontal> <TooltipHost
<div className="ring-container"> content={
<div className={statusColorAnimation}></div> containerInfo?.status === ContainerStatusType.Active
<Icon iconName="LocationDot" className={statusColor} /> ? `Connected to temporary workspace. This temporary workspace will get disconnected in ${Math.round(
</div> containerInfo.durationLeftInMinutes
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}> )} minutes.`
{connectionInfo.status} : toolTipContent
</span> }
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && ( >
<ProgressIndicator description={minute + ":" + second} /> <ActionButton
)} id={buttonId}
</Stack> className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
</TooltipHost> onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
}
>
<Stack className="connectionStatusContainer" horizontal>
<i className={statusColor}></i>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
<ProgressIndicator
className={totalGB !== 0 && usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={totalGB !== 0 ? usedGB / totalGB : 0}
/>
)}
</Stack>
{!isBarDismissed &&
containerInfo.status &&
containerInfo.status === ContainerStatusType.Active &&
Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert ? (
<FocusTrapCallout
role="alertdialog"
className={styles.callout}
gapSpace={0}
target={`#${buttonId}`}
onDismiss={() => setIsBarDismissed(true)}
setInitialFocus
>
<Text block variant="xLarge" className={styles.title}>
Remaining Time
</Text>
<Text block variant="small">
This temporary workspace will get disconnected in {Math.round(containerInfo.durationLeftInMinutes)}{" "}
minutes. To save your work permanently, save your notebooks to a GitHub repository or download the
notebooks to your local machine before the session ends.
</Text>
<FocusZone handleTabKey={FocusZoneTabbableElements.all} isCircularNavigation>
<Stack className={styles.buttons} gap={8} horizontal>
<DefaultButton onClick={() => setIsBarDismissed(true)}>Dimiss</DefaultButton>
</Stack>
</FocusZone>
</FocusTrapCallout>
) : undefined}
</ActionButton>
</TooltipHost>
</>
); );
}; };

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable"; import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
// Vendor modules // Vendor modules
import { import {
@@ -30,6 +31,19 @@ export interface NotebookComponentBootstrapperOptions {
contentRef: ContentRef; contentRef: ContentRef;
} }
interface IWrapModel {
name: string;
path: string;
last_modified: Date;
created: string;
content: unknown;
format: string;
mimetype: unknown;
size: number;
writeable: boolean;
type: string;
}
export class NotebookComponentBootstrapper { export class NotebookComponentBootstrapper {
public contentRef: ContentRef; public contentRef: ContentRef;
protected renderExtraComponent: () => JSX.Element; protected renderExtraComponent: () => JSX.Element;
@@ -41,7 +55,7 @@ export class NotebookComponentBootstrapper {
this.contentRef = options.contentRef; this.contentRef = options.contentRef;
} }
protected static wrapModelIntoContent(name: string, path: string, content: any) { protected static wrapModelIntoContent(name: string, path: string, content: unknown): IWrapModel {
return { return {
name, name,
path, path,
@@ -49,7 +63,7 @@ export class NotebookComponentBootstrapper {
created: "", created: "",
content, content,
format: "json", format: "json",
mimetype: null as any, mimetype: undefined,
size: 0, size: 0,
writeable: false, writeable: false,
type: "notebook", type: "notebook",
@@ -85,7 +99,7 @@ export class NotebookComponentBootstrapper {
}; };
} }
public setContent(name: string, content: any): void { public setContent(name: string, content: unknown): void {
this.getStore().dispatch( this.getStore().dispatch(
actions.fetchContentFulfilled({ actions.fetchContentFulfilled({
filepath: undefined, filepath: undefined,
@@ -270,7 +284,6 @@ export class NotebookComponentBootstrapper {
public isContentDirty(): boolean { public isContentDirty(): boolean {
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef }); const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
if (!content) { if (!content) {
console.log("No error");
return false; return false;
} }

View File

@@ -1,20 +1,39 @@
/** /**
* 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, 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,
IAccountData,
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 { useNotebook } from "./useNotebook"; 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);
@@ -35,14 +54,27 @@ 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 (useNotebook.getState().isPhoenix) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
}
}, delayMs); }, delayMs);
} }
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> { public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
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";
@@ -56,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: {
@@ -75,31 +128,43 @@ export class NotebookContainerClient {
freeKB: memoryUsageInfo.free, freeKB: memoryUsageInfo.free,
}; };
} }
} else if (response.status === HttpStatusCodes.NotFound) {
throw new AbortError(response.statusText);
} }
return undefined; throw new Error(response.statusText);
} catch (error) { } else {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
if (!this.clearReconnectionAttemptMessage) {
this.clearReconnectionAttemptMessage = logConsoleProgress(
"Connection lost with Notebook server. Attempting to reconnect..."
);
}
this.onConnectionLost();
return undefined; return undefined;
} }
} }
public async resetWorkspace(): Promise<void> { private checkStatus(): boolean {
if (useNotebook.getState().isPhoenix) {
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";
@@ -107,15 +172,22 @@ export class NotebookContainerClient {
return Promise.reject(error); return Promise.reject(error);
} }
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try { try {
await fetch(`${notebookServerEndpoint}/api/shutdown`, { if (useNotebook.getState().isPhoenix) {
method: "POST", const provisionData: IProvisionData = {
headers: { Authorization: authToken }, cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
}); };
const accountData: IAccountData = {
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
};
return await this.phoenixClient.resetContainer(provisionData, accountData);
}
return null;
} catch (error) { } catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace"); Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
await this.recreateNotebookWorkspaceAsync(); throw error;
} }
} }
@@ -147,4 +219,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",
};
}
} }

View File

@@ -212,6 +212,7 @@ export default class NotebookManager {
"Cancel", "Cancel",
() => reject(new Error("Commit dialog canceled")), () => reject(new Error("Commit dialog canceled")),
undefined, undefined,
undefined,
{ {
label: "Commit message", label: "Commit message",
autoAdjustHeight: true, autoAdjustHeight: true,

View File

@@ -16,9 +16,10 @@ import "./NotebookReadOnlyRenderer.less";
import SandboxOutputs from "./outputs/SandboxOutputs"; import SandboxOutputs from "./outputs/SandboxOutputs";
export interface NotebookRendererProps { export interface NotebookRendererProps {
contentRef: any; contentRef: ContentRef;
hideInputs?: boolean; hideInputs?: boolean;
hidePrompts?: boolean; hidePrompts?: boolean;
addTransform: (component: React.ComponentType & { MIMETYPE: string }) => void;
} }
/** /**
@@ -27,7 +28,7 @@ export interface NotebookRendererProps {
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> { class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
componentDidMount() { componentDidMount() {
if (!userContext.features.sandboxNotebookOutputs) { if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as any); loadTransform(this.props as NotebookRendererProps);
} }
} }
@@ -59,7 +60,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<div className="NotebookReadOnlyRender"> <div className="NotebookReadOnlyRender">
<Cells contentRef={this.props.contentRef}> <Cells contentRef={this.props.contentRef}>
{{ {{
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => ( code: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
<CodeCell id={id} contentRef={contentRef}> <CodeCell id={id} contentRef={contentRef}>
{{ {{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef), prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
@@ -73,14 +74,14 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
}} }}
</CodeCell> </CodeCell>
), ),
markdown: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => ( markdown: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown"> <MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
{{ {{
editor: {}, editor: {},
}} }}
</MarkdownCell> </MarkdownCell>
), ),
raw: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => ( raw: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
<RawCell id={id} contentRef={contentRef} cell_type="raw"> <RawCell id={id} contentRef={contentRef} cell_type="raw">
{{ {{
editor: { editor: {
@@ -98,6 +99,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => { const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
const mapDispatchToProps = (dispatch: Dispatch) => { const mapDispatchToProps = (dispatch: Dispatch) => {
return { return {
@@ -114,4 +116,4 @@ const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: Noteboo
return mapDispatchToProps; return mapDispatchToProps;
}; };
export default connect(null, makeMapDispatchToProps)(NotebookReadOnlyRenderer); export default connect(undefined, makeMapDispatchToProps)(NotebookReadOnlyRenderer);

View File

@@ -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,

View File

@@ -1,11 +1,15 @@
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { ConnectionStatusType } 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 { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo, ContainerInfo, IAccountData } 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";
@@ -28,8 +32,12 @@ interface NotebookState {
myNotebooksContentRoot: NotebookContentItem; myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem;
connectionInfo: DataModels.ContainerConnectionInfo; connectionInfo: ContainerConnectionInfo;
notebookFolderName: string; notebookFolderName: string;
isAllocating: boolean;
isRefreshed: boolean;
containerStatus: ContainerInfo;
isPhoenix: boolean;
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;
@@ -46,7 +54,13 @@ interface NotebookState {
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void; setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
setIsAllocating: (isAllocating: boolean) => void;
resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void;
setIsRefreshed: (isAllocating: boolean) => void;
setContainerStatus: (containerStatus: ContainerInfo) => void;
getPhoenixStatus: () => Promise<void>;
setIsPhoenix: (isPhoenix: boolean) => void;
} }
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -55,6 +69,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,
@@ -69,8 +84,18 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
myNotebooksContentRoot: undefined, myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined, gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined, galleryContentRoot: undefined,
connectionInfo: undefined, connectionInfo: {
status: ConnectionStatusType.Connect,
},
notebookFolderName: undefined, notebookFolderName: undefined,
isAllocating: false,
isRefreshed: false,
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
},
isPhoenix: 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) =>
@@ -83,6 +108,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }), setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }), setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => { refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
await get().getPhoenixStatus();
const { databaseAccount, authType } = userContext; const { databaseAccount, authType } = userContext;
if ( if (
authType === AuthType.EncryptedToken || authType === AuthType.EncryptedToken ||
@@ -175,7 +201,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
}, },
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => { initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = userContext.features.phoenix === true ? "Temporary Notebooks" : "My Notebooks"; const notebookFolderName = "Temporary Notebooks";
set({ notebookFolderName }); set({ notebookFolderName });
const myNotebooksContentRoot = { const myNotebooksContentRoot = {
name: get().notebookFolderName, name: get().notebookFolderName,
@@ -256,5 +282,32 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
set({ gitHubNotebooksContentRoot }); set({ gitHubNotebooksContentRoot });
} }
}, },
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }), setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
useTabs.getState().closeAllNotebookTabs(true);
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo(undefined);
useNotebook.getState().setIsAllocating(false);
useNotebook.getState().setContainerStatus({
status: undefined,
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
});
},
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
getPhoenixStatus: async () => {
if (get().isPhoenix === undefined) {
const phoenixClient = new PhoenixClient();
const accountData: IAccountData = {
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
};
const isPhoenix = await phoenixClient.IsDbAcountWhitelisted(accountData);
set({ isPhoenix });
}
},
setIsPhoenix: (isPhoenix: boolean) => set({ isPhoenix }),
})); }));

View File

@@ -4,6 +4,8 @@ import * as React from "react";
import { useFullScreenURLs } from "../hooks/useFullScreenURLs"; import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
export const OpenFullScreen: React.FunctionComponent = () => { export const OpenFullScreen: React.FunctionComponent = () => {
const [isReadUrlCopy, setIsReadUrlCopy] = React.useState<boolean>(false);
const [isReadWriteUrlCopy, setIsReadWriteUrlCopy] = React.useState<boolean>(false);
const result = useFullScreenURLs(); const result = useFullScreenURLs();
if (!result) { if (!result) {
return <Spinner label="Generating URLs..." ariaLive="assertive" labelPosition="right" />; return <Spinner label="Generating URLs..." ariaLive="assertive" labelPosition="right" />;
@@ -25,8 +27,9 @@ export const OpenFullScreen: React.FunctionComponent = () => {
<DefaultButton <DefaultButton
onClick={() => { onClick={() => {
copyToClipboard(readWriteUrl); copyToClipboard(readWriteUrl);
setIsReadWriteUrlCopy(true);
}} }}
text="Copy" text={isReadWriteUrlCopy ? "Copied" : "Copy"}
iconProps={{ iconName: "Copy" }} iconProps={{ iconName: "Copy" }}
/> />
<PrimaryButton <PrimaryButton
@@ -41,9 +44,10 @@ export const OpenFullScreen: React.FunctionComponent = () => {
<Stack horizontal tokens={{ childrenGap: 10 }}> <Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton <DefaultButton
onClick={() => { onClick={() => {
setIsReadUrlCopy(true);
copyToClipboard(readUrl); copyToClipboard(readUrl);
}} }}
text="Copy" text={isReadUrlCopy ? "Copied" : "Copy"}
iconProps={{ iconName: "Copy" }} iconProps={{ iconName: "Copy" }}
/> />
<PrimaryButton <PrimaryButton

View File

@@ -13,21 +13,21 @@ import {
Text, Text,
TooltipHost, TooltipHost,
} from "@fluentui/react"; } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { createCollection } from "Common/dataAccess/createCollection";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { SubscriptionType } from "Contracts/SubscriptionType";
import { useSidePanel } from "hooks/useSidePanel";
import React from "react"; import React from "react";
import * as Constants from "../../Common/Constants"; import { CollectionCreation } from "Shared/Constants";
import { createCollection } from "../../Common/dataAccess/createCollection"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { configContext, Platform } from "../../ConfigContext"; import { userContext } from "UserContext";
import * as DataModels from "../../Contracts/DataModels"; import { getCollectionName } from "Utils/APITypeUtils";
import { SubscriptionType } from "../../Contracts/SubscriptionType"; import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils";
import { useSidePanel } from "../../hooks/useSidePanel"; import { getUpsellMessage } from "Utils/PricingUtils";
import { CollectionCreation } from "../../Shared/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount } from "../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@@ -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)}
/> />
)} )}
@@ -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;
}} }}
@@ -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>
@@ -999,7 +1007,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
const collectionId: string = this.state.collectionId.trim(); const collectionId: string = this.state.collectionId.trim();
let databaseId = this.state.createNewDatabase ? this.state.newDatabaseId.trim() : this.state.selectedDatabaseId; let databaseId = this.state.createNewDatabase ? this.state.newDatabaseId.trim() : this.state.selectedDatabaseId;
let partitionKeyString = this.state.partitionKey.trim(); let partitionKeyString = this.state.isSharded ? this.state.partitionKey.trim() : undefined;
if (userContext.apiType === "Tables") { if (userContext.apiType === "Tables") {
// Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk' // Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk'

View File

@@ -23,12 +23,10 @@ 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;
@@ -52,6 +50,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;
@@ -79,7 +78,6 @@ 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);
buttonElement.focus();
}, []); }, []);
const onSubmit = () => { const onSubmit = () => {
@@ -169,6 +167,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
formError: formErrors, formError: formErrors,
isExecuting, isExecuting,
submitButtonText: "OK", submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit, onSubmit,
}; };
@@ -239,6 +238,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)}
/> />
)} )}

View File

@@ -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>

View File

@@ -1,14 +1,14 @@
import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } from "@fluentui/react"; import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as SharedConstants from "Shared/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { userContext } from "UserContext";
import * as SharedConstants from "../../../Shared/Constants"; import { isServerlessAccount } from "Utils/CapabilityUtils";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
@@ -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)}
/> />
)} )}

View File

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

View File

@@ -1,18 +1,18 @@
import { Text, TextField } from "@fluentui/react"; import { Text, TextField } from "@fluentui/react";
import { Areas } from "Common/Constants";
import { deleteCollection } from "Common/dataAccess/deleteCollection";
import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { Collection } from "Contracts/ViewModels";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../../Common/Constants"; import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection"; import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import DeleteFeedback from "../../../Common/DeleteFeedback"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { userContext } from "UserContext";
import { Collection } from "../../../Contracts/ViewModels"; import { getCollectionName } from "Utils/APITypeUtils";
import { useSidePanel } from "../../../hooks/useSidePanel"; import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import { useTabs } from "../../../hooks/useTabs";
import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";

View File

@@ -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}

View File

@@ -1,18 +1,18 @@
import { Text, TextField } from "@fluentui/react"; import { Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { Areas } from "Common/Constants";
import { deleteDatabase } from "Common/dataAccess/deleteDatabase";
import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { Collection, Database } from "Contracts/ViewModels";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../Common/Constants"; import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase"; import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import DeleteFeedback from "../../Common/DeleteFeedback"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { userContext } from "UserContext";
import { Collection, Database } from "../../Contracts/ViewModels"; import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";

View File

@@ -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}

View File

@@ -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);

View File

@@ -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],

View File

@@ -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>
); );

View File

@@ -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 />}
</> </>

View File

@@ -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}

View File

@@ -1,13 +1,13 @@
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react"; import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, MouseEvent, useState } from "react"; import React, { FunctionComponent, MouseEvent, useState } from "react";
import * as Constants from "../../../Common/Constants"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; import * as StringUtility from "Shared/StringUtility";
import { configContext } from "../../../ConfigContext"; import { userContext } from "UserContext";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import * as StringUtility from "../../../Shared/StringUtility";
import { userContext } from "../../../UserContext";
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export const SettingsPane: FunctionComponent = () => { export const SettingsPane: FunctionComponent = () => {

View File

@@ -1,8 +1,8 @@
import { TextField } from "@fluentui/react"; import { TextField } from "@fluentui/react";
import * as ViewModels from "Contracts/ViewModels";
import { useTabs } from "hooks/useTabs";
import React, { FormEvent, FunctionComponent, useState } from "react"; import React, { FormEvent, FunctionComponent, useState } from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
import { useTabs } from "../../../hooks/useTabs";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import NotebookV2Tab from "../../Tabs/NotebookV2Tab"; import NotebookV2Tab from "../../Tabs/NotebookV2Tab";

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -1,7 +1,7 @@
import { Upload } from "Common/Upload/Upload";
import { useSidePanel } from "hooks/useSidePanel";
import React, { ChangeEvent, FunctionComponent, useState } from "react"; import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload/Upload"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";

View File

@@ -1,8 +1,8 @@
import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "@fluentui/react"; import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "@fluentui/react";
import { Upload } from "Common/Upload/Upload";
import { UploadDetailsRecord } from "Contracts/ViewModels";
import React, { ChangeEvent, FunctionComponent, useState } from "react"; import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload/Upload"; import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { getErrorMessage } from "../../Tables/Utilities"; import { getErrorMessage } from "../../Tables/Utilities";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";

View File

@@ -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}

View File

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

View File

@@ -1,12 +1,11 @@
import _ from "underscore";
import Q from "q"; import Q from "q";
import _ from "underscore";
import * as Entities from "../Entities";
import * as QueryBuilderConstants from "../Constants"; import * as QueryBuilderConstants from "../Constants";
import * as Entities from "../Entities";
import * as Utilities from "../Utilities"; import * as Utilities from "../Utilities";
export function getRowSelector(selectorSchema: Entities.IProperty[]): string { export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
var selector: string = ""; let selector = "";
selectorSchema && selectorSchema &&
selectorSchema.forEach((p: Entities.IProperty) => { selectorSchema.forEach((p: Entities.IProperty) => {
selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]'; selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]';
@@ -15,10 +14,10 @@ export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
} }
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean { export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean {
var isVisible = false; let isVisible = false;
if (dataTableScrollBodyQuery.length && element) { if (dataTableScrollBodyQuery.length && element) {
var elementRect: ClientRect = element.getBoundingClientRect(), const elementRect: ClientRect = element.getBoundingClientRect(),
dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect(); dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect();
isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top; isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top;
@@ -29,17 +28,17 @@ export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElem
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void { export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void {
if (dataTableRows.length) { if (dataTableRows.length) {
var dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector), const dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex); selectedRowElement: HTMLElement = dataTableRows.get(currentIndex);
if (dataTableScrollBodyQuery.length && selectedRowElement) { if (dataTableScrollBodyQuery.length && selectedRowElement) {
var isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement); const isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
if (!isVisible) { if (!isVisible) {
var selectedRowQuery: JQuery = $(selectedRowElement), const selectedRowQuery: JQuery = $(selectedRowElement),
scrollPosition: number = dataTableScrollBodyQuery.scrollTop(), scrollPosition: number = dataTableScrollBodyQuery.scrollTop(),
selectedElementPosition: number = selectedRowQuery.position().top, selectedElementPosition: number = selectedRowQuery.position().top;
newScrollPosition: number = 0; let newScrollPosition = 0;
if (isScrollUp) { if (isScrollUp) {
newScrollPosition = scrollPosition + selectedElementPosition; newScrollPosition = scrollPosition + selectedElementPosition;
@@ -55,7 +54,7 @@ export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number,
} }
export function scrollToTopIfNeeded(): void { export function scrollToTopIfNeeded(): void {
var $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector), const $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector); $dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
if ($dataTableRows.length && $dataTableScrollBody.length) { if ($dataTableRows.length && $dataTableScrollBody.length) {
@@ -88,13 +87,14 @@ export function reorderColumns(
table: DataTables.DataTable, table: DataTables.DataTable,
targetOrder: number[], targetOrder: number[],
currentOrder?: number[] currentOrder?: number[]
//eslint-disable-next-line
): Q.Promise<any> { ): Q.Promise<any> {
var columnsCount: number = targetOrder.length; const columnsCount: number = targetOrder.length;
var isCurrentOrderPassedIn: boolean = !!currentOrder; const isCurrentOrderPassedIn = !!currentOrder;
if (!isCurrentOrderPassedIn) { if (!isCurrentOrderPassedIn) {
currentOrder = getInitialOrder(columnsCount); currentOrder = getInitialOrder(columnsCount);
} }
var isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder); const isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder);
// if the targetOrder is the same as current order, do nothing. // if the targetOrder is the same as current order, do nothing.
if (!isSameOrder) { if (!isSameOrder) {
@@ -104,7 +104,7 @@ export function reorderColumns(
// Then the transformation order will be the same as target order. // Then the transformation order will be the same as target order.
// If current order is specified, then a transformation order is calculated. // If current order is specified, then a transformation order is calculated.
// Refer to calculateTransformationOrder for details about transformation order. // Refer to calculateTransformationOrder for details about transformation order.
var transformationOrder: number[] = isCurrentOrderPassedIn const transformationOrder: number[] = isCurrentOrderPassedIn
? calculateTransformationOrder(currentOrder, targetOrder) ? calculateTransformationOrder(currentOrder, targetOrder)
: targetOrder; : targetOrder;
try { try {
@@ -143,7 +143,7 @@ export function getCurrentOrder(table: DataTables.DataTable): number[] {
* Result: [0, 1, 2, 5, 6, 7, 3, 4, 8] * Result: [0, 1, 2, 5, 6, 7, 3, 4, 8]
*/ */
export function invertIndexValues(inputArray: number[]): number[] { export function invertIndexValues(inputArray: number[]): number[] {
var invertedArray: number[] = []; const invertedArray: number[] = [];
if (inputArray) { if (inputArray) {
inputArray.forEach((value: number, index: number) => { inputArray.forEach((value: number, index: number) => {
invertedArray[inputArray[index]] = index; invertedArray[inputArray[index]] = index;
@@ -170,20 +170,21 @@ export function invertIndexValues(inputArray: number[]): number[] {
* transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6] * transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6]
*/ */
export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] { export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] {
var transformationOrder: number[] = []; let transformationOrder: number[] = [];
if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) { if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) {
var invertedCurrentOrder: number[] = invertIndexValues(currentOrder); const invertedCurrentOrder: number[] = invertIndexValues(currentOrder);
transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]); transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]);
} }
return transformationOrder; return transformationOrder;
} }
export function getDataTableHeaders(table: DataTables.DataTable): string[] { export function getDataTableHeaders(table: DataTables.DataTable): string[] {
var columns: DataTables.ColumnsMethods = table.columns(); const columns: DataTables.ColumnsMethods = table.columns();
var headers: string[] = []; let headers: string[] = [];
if (columns) { if (columns) {
// table.columns() return ColumnsMethods which is an array of arrays // table.columns() return ColumnsMethods which is an array of arrays
var columnIndexes: number[] = (<any>columns)[0]; //eslint-disable-next-line
const columnIndexes: number[] = (<any>columns)[0];
if (columnIndexes) { if (columnIndexes) {
headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html()); headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html());
} }

View File

@@ -8,11 +8,11 @@ import TableEntityListViewModel from "./TableEntityListViewModel";
export default class TableCommands { export default class TableCommands {
// Command Ids // Command Ids
public static editEntityCommand: string = "edit"; public static editEntityCommand = "edit";
public static deleteEntitiesCommand: string = "delete"; public static deleteEntitiesCommand = "delete";
public static reorderColumnsCommand: string = "reorder"; public static reorderColumnsCommand = "reorder";
public static resetColumnsCommand: string = "reset"; public static resetColumnsCommand = "reset";
public static customizeColumnsCommand: string = "customizeColumns"; public static customizeColumnsCommand = "customizeColumns";
private _container: Explorer; private _container: Explorer;
@@ -21,8 +21,8 @@ export default class TableCommands {
} }
public isEnabled(commandName: string, selectedEntites: Entities.ITableEntity[]): boolean { public isEnabled(commandName: string, selectedEntites: Entities.ITableEntity[]): boolean {
var singleItemSelected: boolean = DataTableUtilities.containSingleItem(selectedEntites); const singleItemSelected = DataTableUtilities.containSingleItem(selectedEntites);
var atLeastOneItemSelected: boolean = DataTableUtilities.containItems(selectedEntites); const atLeastOneItemSelected = DataTableUtilities.containItems(selectedEntites);
switch (commandName) { switch (commandName) {
case TableCommands.editEntityCommand: case TableCommands.editEntityCommand:
return singleItemSelected; return singleItemSelected;
@@ -47,6 +47,7 @@ export default class TableCommands {
/** /**
* Edit entity * Edit entity
*/ */
//eslint-disable-next-line
public editEntityCommand(viewModel: TableEntityListViewModel): Q.Promise<any> { public editEntityCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
if (!viewModel) { if (!viewModel) {
return null; // Error return null; // Error
@@ -56,12 +57,9 @@ export default class TableCommands {
return null; // Erorr return null; // Erorr
} }
var entityToUpdate: Entities.ITableEntity = viewModel.selected()[0];
var originalNumberOfProperties = entityToUpdate ? 0 : Object.keys(entityToUpdate).length - 1; // .metadata is always a property for etag
return null; return null;
} }
//eslint-disable-next-line
public deleteEntitiesCommand(viewModel: TableEntityListViewModel): Q.Promise<any> { public deleteEntitiesCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
if (!viewModel) { if (!viewModel) {
return null; // Error return null; // Error
@@ -69,7 +67,7 @@ export default class TableCommands {
if (!DataTableUtilities.containItems(viewModel.selected())) { if (!DataTableUtilities.containItems(viewModel.selected())) {
return null; // Error return null; // Error
} }
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); const entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
const deleteMessage: string = const deleteMessage: string =
userContext.apiType === "Cassandra" userContext.apiType === "Cassandra"
? "Are you sure you want to delete the selected rows?" ? "Are you sure you want to delete the selected rows?"
@@ -82,7 +80,7 @@ export default class TableCommands {
() => { () => {
viewModel.queryTablesTab.container.tableDataClient viewModel.queryTablesTab.container.tableDataClient
.deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete)
.then((results: any) => { .then(() => {
return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => { return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => {
viewModel.redrawTableThrottled(); viewModel.redrawTableThrottled();
}); });

View File

@@ -1,5 +1,5 @@
import * as Utilities from "../Utilities";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
import * as Utilities from "../Utilities";
import CacheBase from "./CacheBase"; import CacheBase from "./CacheBase";
export default class TableEntityCache extends CacheBase<Entities.ITableEntity> { export default class TableEntityCache extends CacheBase<Entities.ITableEntity> {
@@ -21,7 +21,7 @@ export default class TableEntityCache extends CacheBase<Entities.ITableEntity> {
this._tableQuery = Utilities.copyTableQuery(tableQuery); this._tableQuery = Utilities.copyTableQuery(tableQuery);
} }
public preClear() { public preClear(): void {
this.tableQuery = null; this.tableQuery = null;
} }
} }

View File

@@ -1,4 +1,4 @@
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
export interface ITableEntity { export interface ITableEntity {
[property: string]: ITableEntityAttribute; [property: string]: ITableEntityAttribute;
@@ -17,6 +17,7 @@ export interface ITableEntityAttribute {
export interface IListTableEntitiesResult { export interface IListTableEntitiesResult {
Results: ITableEntity[]; Results: ITableEntity[];
//eslint-disable-next-line
ContinuationToken: any; ContinuationToken: any;
iterator?: QueryIterator<ItemDefinition & Resource>; iterator?: QueryIterator<ItemDefinition & Resource>;
} }

View File

@@ -1,8 +1,9 @@
import QueryClauseViewModel from "./QueryClauseViewModel";
import * as Utilities from "../Utilities"; import * as Utilities from "../Utilities";
import QueryClauseViewModel from "./QueryClauseViewModel";
export default class ClauseGroup { export default class ClauseGroup {
public isRootGroup: boolean; public isRootGroup: boolean;
//eslint-disable-next-line
public children = new Array(); public children = new Array();
public parentGroup: ClauseGroup; public parentGroup: ClauseGroup;
private _id: string; private _id: string;
@@ -17,7 +18,7 @@ export default class ClauseGroup {
* Flattens the clause tree into an array, depth-first, left to right. * Flattens the clause tree into an array, depth-first, left to right.
*/ */
public flattenClauses(targetArray: ko.ObservableArray<QueryClauseViewModel>): void { public flattenClauses(targetArray: ko.ObservableArray<QueryClauseViewModel>): void {
var tempArray = new Array<QueryClauseViewModel>(); const tempArray = new Array<QueryClauseViewModel>();
this.flattenClausesImpl(this, tempArray); this.flattenClausesImpl(this, tempArray);
targetArray.removeAll(); targetArray.removeAll();
@@ -31,10 +32,10 @@ export default class ClauseGroup {
newClause.clauseGroup = this; newClause.clauseGroup = this;
this.children.push(newClause); this.children.push(newClause);
} else { } else {
var targetGroup = insertBefore.clauseGroup; const targetGroup = insertBefore.clauseGroup;
if (targetGroup) { if (targetGroup) {
var insertBeforeIndex = targetGroup.children.indexOf(insertBefore); const insertBeforeIndex = targetGroup.children.indexOf(insertBefore);
newClause.clauseGroup = targetGroup; newClause.clauseGroup = targetGroup;
targetGroup.children.splice(insertBeforeIndex, 0, newClause); targetGroup.children.splice(insertBeforeIndex, 0, newClause);
} }
@@ -42,19 +43,19 @@ export default class ClauseGroup {
} }
public deleteClause(clause: QueryClauseViewModel): void { public deleteClause(clause: QueryClauseViewModel): void {
var targetGroup = clause.clauseGroup; const targetGroup = clause.clauseGroup;
if (targetGroup) { if (targetGroup) {
var index = targetGroup.children.indexOf(clause); const index = targetGroup.children.indexOf(clause);
targetGroup.children.splice(index, 1); targetGroup.children.splice(index, 1);
clause.dispose(); clause.dispose();
if (targetGroup.children.length <= 1 && !targetGroup.isRootGroup) { if (targetGroup.children.length <= 1 && !targetGroup.isRootGroup) {
var parent = targetGroup.parentGroup; const parent = targetGroup.parentGroup;
var targetGroupIndex = parent.children.indexOf(targetGroup); const targetGroupIndex = parent.children.indexOf(targetGroup);
if (targetGroup.children.length === 1) { if (targetGroup.children.length === 1) {
var orphan = targetGroup.children.shift(); const orphan = targetGroup.children.shift();
if (orphan instanceof QueryClauseViewModel) { if (orphan instanceof QueryClauseViewModel) {
(<QueryClauseViewModel>orphan).clauseGroup = parent; (<QueryClauseViewModel>orphan).clauseGroup = parent;
@@ -71,14 +72,14 @@ export default class ClauseGroup {
} }
public removeAll(): void { public removeAll(): void {
var allClauses: QueryClauseViewModel[] = new Array<QueryClauseViewModel>(); const allClauses: QueryClauseViewModel[] = new Array<QueryClauseViewModel>();
this.flattenClausesImpl(this, allClauses); this.flattenClausesImpl(this, allClauses);
while (allClauses.length > 0) { while (allClauses.length > 0) {
allClauses.shift().dispose(); allClauses.shift().dispose();
} }
//eslint-disable-next-line
this.children = new Array<any>(); this.children = new Array<any>();
} }
@@ -87,12 +88,12 @@ export default class ClauseGroup {
*/ */
public groupSelectedItems(): boolean { public groupSelectedItems(): boolean {
// Find the selection start & end, also check for gaps between selected items (if found, cannot proceed). // Find the selection start & end, also check for gaps between selected items (if found, cannot proceed).
var selection = this.getCheckedItemsInfo(); const selection = this.getCheckedItemsInfo();
if (selection.canGroup) { if (selection.canGroup) {
var newGroup = new ClauseGroup(false, this); const newGroup = new ClauseGroup(false, this);
// Replace the selected items with the new group, and then move the selected items into the new group. // Replace the selected items with the new group, and then move the selected items into the new group.
var groupedItems = this.children.splice(selection.begin, selection.end - selection.begin + 1, newGroup); const groupedItems = this.children.splice(selection.begin, selection.end - selection.begin + 1, newGroup);
groupedItems && groupedItems &&
groupedItems.forEach((element) => { groupedItems.forEach((element) => {
@@ -118,13 +119,13 @@ export default class ClauseGroup {
return; return;
} }
var parentGroup = this.parentGroup; const parentGroup = this.parentGroup;
var index = parentGroup.children.indexOf(this); let index = parentGroup.children.indexOf(this);
if (index >= 0) { if (index >= 0) {
parentGroup.children.splice(index, 1); parentGroup.children.splice(index, 1);
var toPromote = this.children.splice(0, this.children.length); const toPromote = this.children.splice(0, this.children.length);
// Move all children one level up. // Move all children one level up.
toPromote && toPromote &&
@@ -146,16 +147,16 @@ export default class ClauseGroup {
} }
public findDeepestGroupInChildren(skipIndex?: number): ClauseGroup { public findDeepestGroupInChildren(skipIndex?: number): ClauseGroup {
var deepest: ClauseGroup = this; let deepest = <ClauseGroup>this;
var level: number = 0; let level = 0;
var func = (currentGroup: ClauseGroup): void => { const func = (currentGroup: ClauseGroup): void => {
level++; level++;
if (currentGroup.getCurrentGroupDepth() > deepest.getCurrentGroupDepth()) { if (currentGroup.getCurrentGroupDepth() > deepest.getCurrentGroupDepth()) {
deepest = currentGroup; deepest = currentGroup;
} }
for (var i = 0; i < currentGroup.children.length; i++) { for (let i = 0; i < currentGroup.children.length; i++) {
var currentItem = currentGroup.children[i]; const currentItem = currentGroup.children[i];
if ((i !== skipIndex || level > 1) && currentItem instanceof ClauseGroup) { if ((i !== skipIndex || level > 1) && currentItem instanceof ClauseGroup) {
func(currentItem); func(currentItem);
@@ -170,16 +171,16 @@ export default class ClauseGroup {
} }
private getCheckedItemsInfo(): { canGroup: boolean; begin: number; end: number } { private getCheckedItemsInfo(): { canGroup: boolean; begin: number; end: number } {
var beginIndex = -1; let beginIndex = -1;
var endIndex = -1; let endIndex = -1;
// In order to perform group, all selected items must be next to each other. // In order to perform group, all selected items must be next to each other.
// If one or more items are not selected between the first and the last selected item, the gapFlag will be set to True, meaning cannot perform group. // If one or more items are not selected between the first and the last selected item, the gapFlag will be set to True, meaning cannot perform group.
var gapFlag = false; let gapFlag = false;
var count = 0; let count = 0;
for (var i = 0; i < this.children.length; i++) { for (let i = 0; i < this.children.length; i++) {
var currentItem = this.children[i]; const currentItem = this.children[i];
var subGroupSelectionState: { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean }; let subGroupSelectionState: { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean };
if (currentItem instanceof ClauseGroup) { if (currentItem instanceof ClauseGroup) {
subGroupSelectionState = (<ClauseGroup>currentItem).getSelectionState(); subGroupSelectionState = (<ClauseGroup>currentItem).getSelectionState();
@@ -235,10 +236,10 @@ export default class ClauseGroup {
} }
private getSelectionState(): { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean } { private getSelectionState(): { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean } {
var selectedCount = 0; let selectedCount = 0;
for (var i = 0; i < this.children.length; i++) { for (let i = 0; i < this.children.length; i++) {
var currentItem = this.children[i]; const currentItem = this.children[i];
if (currentItem instanceof ClauseGroup && (<ClauseGroup>currentItem).getSelectionState().allSelected) { if (currentItem instanceof ClauseGroup && (<ClauseGroup>currentItem).getSelectionState().allSelected) {
selectedCount++; selectedCount++;
@@ -260,8 +261,8 @@ export default class ClauseGroup {
} }
private unselectAll(): void { private unselectAll(): void {
for (var i = 0; i < this.children.length; i++) { for (let i = 0; i < this.children.length; i++) {
var currentItem = this.children[i]; const currentItem = this.children[i];
if (currentItem instanceof ClauseGroup) { if (currentItem instanceof ClauseGroup) {
(<ClauseGroup>currentItem).unselectAll(); (<ClauseGroup>currentItem).unselectAll();
@@ -278,8 +279,8 @@ export default class ClauseGroup {
targetArray.splice(0, targetArray.length); targetArray.splice(0, targetArray.length);
} }
for (var i = 0; i < queryGroup.children.length; i++) { for (let i = 0; i < queryGroup.children.length; i++) {
var currentItem = queryGroup.children[i]; const currentItem = queryGroup.children[i];
if (currentItem instanceof ClauseGroup) { if (currentItem instanceof ClauseGroup) {
this.flattenClausesImpl(currentItem, targetArray); this.flattenClausesImpl(currentItem, targetArray);
@@ -292,13 +293,13 @@ export default class ClauseGroup {
} }
public getTreeDepth(): number { public getTreeDepth(): number {
var currentDepth = this.getCurrentGroupDepth(); let currentDepth = this.getCurrentGroupDepth();
for (var i = 0; i < this.children.length; i++) { for (let i = 0; i < this.children.length; i++) {
var currentItem = this.children[i]; const currentItem = this.children[i];
if (currentItem instanceof ClauseGroup) { if (currentItem instanceof ClauseGroup) {
var newDepth = (<ClauseGroup>currentItem).getTreeDepth(); const newDepth = (<ClauseGroup>currentItem).getTreeDepth();
if (newDepth > currentDepth) { if (newDepth > currentDepth) {
currentDepth = newDepth; currentDepth = newDepth;
@@ -310,8 +311,8 @@ export default class ClauseGroup {
} }
public getCurrentGroupDepth(): number { public getCurrentGroupDepth(): number {
var group = <ClauseGroup>this; let group = <ClauseGroup>this;
var depth = 0; let depth = 0;
while (!group.isRootGroup) { while (!group.isRootGroup) {
depth++; depth++;

View File

@@ -1,7 +1,7 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../Constants";
import ClauseGroup from "./ClauseGroup"; import ClauseGroup from "./ClauseGroup";
import QueryBuilderViewModel from "./QueryBuilderViewModel"; import QueryBuilderViewModel from "./QueryBuilderViewModel";
import * as Constants from "../Constants";
/** /**
* View model for showing group indicators on UI, contains information such as group color and border styles. * View model for showing group indicators on UI, contains information such as group color and border styles.
@@ -38,7 +38,7 @@ export default class ClauseGroupViewModel {
}; };
private getGroupBackgroundColor(group: ClauseGroup): string { private getGroupBackgroundColor(group: ClauseGroup): string {
var colorCount = Constants.clauseGroupColors.length; const colorCount = Constants.clauseGroupColors.length;
if (group.isRootGroup) { if (group.isRootGroup) {
return Constants.transparentColor; return Constants.transparentColor;

View File

@@ -29,7 +29,7 @@ export default class QueryBuilderViewModel {
public removeThisFilterLine = "Remove this filter line"; // localize public removeThisFilterLine = "Remove this filter line"; // localize
public groupSelectedClauses = "Group selected clauses"; // localize public groupSelectedClauses = "Group selected clauses"; // localize
public clauseArray = ko.observableArray<QueryClauseViewModel>(); // This is for storing the clauses in flattened form queryClauses for easier UI data binding. public clauseArray = ko.observableArray<QueryClauseViewModel>(); // This is for storing the clauses in flattened form queryClauses for easier UI data binding.
public queryClauses = new ClauseGroup(true, null); // The actual data structure containing the clause information. public queryClauses = new ClauseGroup(true, undefined); // The actual data structure containing the clause information.
public columnOptions: ko.ObservableArray<string>; public columnOptions: ko.ObservableArray<string>;
public canGroupClauses = ko.observable<boolean>(false); public canGroupClauses = ko.observable<boolean>(false);
@@ -107,7 +107,7 @@ export default class QueryBuilderViewModel {
} }
public setExample() { public setExample() {
var example1 = new QueryClauseViewModel( const example1 = new QueryClauseViewModel(
this, this,
"", "",
"PartitionKey", "PartitionKey",
@@ -121,7 +121,7 @@ export default class QueryBuilderViewModel {
//null, //null,
true true
); );
var example2 = new QueryClauseViewModel( const example2 = new QueryClauseViewModel(
this, this,
"And", "And",
"RowKey", "RowKey",
@@ -140,13 +140,13 @@ export default class QueryBuilderViewModel {
} }
public getODataFilterFromClauses = (): string => { public getODataFilterFromClauses = (): string => {
var filterString: string = ""; let filterString = "";
var treeTraversal = (group: ClauseGroup): void => { const treeTraversal = (group: ClauseGroup): void => {
for (var i = 0; i < group.children.length; i++) { for (let i = 0; i < group.children.length; i++) {
var currentItem = group.children[i]; const currentItem = group.children[i];
if (currentItem instanceof QueryClauseViewModel) { if (currentItem instanceof QueryClauseViewModel) {
var clause = <QueryClauseViewModel>currentItem; const clause = <QueryClauseViewModel>currentItem;
this.timestampToValue(clause); this.timestampToValue(clause);
filterString = filterString.concat( filterString = filterString.concat(
this.constructODataClause( this.constructODataClause(
@@ -173,7 +173,7 @@ export default class QueryBuilderViewModel {
}; };
public getSqlFilterFromClauses = (): string => { public getSqlFilterFromClauses = (): string => {
var filterString: string = "SELECT * FROM c"; let filterString = "SELECT * FROM c";
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) { if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
filterString = "SELECT"; filterString = "SELECT";
const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText(); const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText();
@@ -199,15 +199,15 @@ export default class QueryBuilderViewModel {
return filterString; return filterString;
} }
filterString = filterString.concat(" WHERE"); filterString = filterString.concat(" WHERE");
var first = true; let first = true;
var treeTraversal = (group: ClauseGroup): void => { const treeTraversal = (group: ClauseGroup): void => {
for (var i = 0; i < group.children.length; i++) { for (let i = 0; i < group.children.length; i++) {
var currentItem = group.children[i]; const currentItem = group.children[i];
if (currentItem instanceof QueryClauseViewModel) { if (currentItem instanceof QueryClauseViewModel) {
var clause = <QueryClauseViewModel>currentItem; const clause = <QueryClauseViewModel>currentItem;
let timeStampValue: string = this.timestampToSqlValue(clause); const timeStampValue: string = this.timestampToSqlValue(clause);
var value = clause.value(); let value = clause.value();
if (!clause.isValue()) { if (!clause.isValue()) {
value = timeStampValue; value = timeStampValue;
} }
@@ -240,7 +240,7 @@ export default class QueryBuilderViewModel {
const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId; const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId;
const collectionId = this._queryViewModel.queryTablesTab.collection.id(); const collectionId = this._queryViewModel.queryTablesTab.collection.id();
const tableToQuery = `${getQuotedCqlIdentifier(databaseId)}.${getQuotedCqlIdentifier(collectionId)}`; const tableToQuery = `${getQuotedCqlIdentifier(databaseId)}.${getQuotedCqlIdentifier(collectionId)}`;
var filterString: string = `SELECT * FROM ${tableToQuery}`; let filterString = `SELECT * FROM ${tableToQuery}`;
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) { if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
filterString = "SELECT"; filterString = "SELECT";
const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText(); const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText();
@@ -255,15 +255,15 @@ export default class QueryBuilderViewModel {
return filterString; return filterString;
} }
filterString = filterString.concat(" WHERE"); filterString = filterString.concat(" WHERE");
var first = true; let first = true;
var treeTraversal = (group: ClauseGroup): void => { const treeTraversal = (group: ClauseGroup): void => {
for (var i = 0; i < group.children.length; i++) { for (let i = 0; i < group.children.length; i++) {
var currentItem = group.children[i]; const currentItem = group.children[i];
if (currentItem instanceof QueryClauseViewModel) { if (currentItem instanceof QueryClauseViewModel) {
var clause = <QueryClauseViewModel>currentItem; const clause = <QueryClauseViewModel>currentItem;
let timeStampValue: string = this.timestampToSqlValue(clause); const timeStampValue = this.timestampToSqlValue(clause);
var value = clause.value(); let value = clause.value();
if (!clause.isValue()) { if (!clause.isValue()) {
value = timeStampValue; value = timeStampValue;
} }
@@ -293,13 +293,13 @@ export default class QueryBuilderViewModel {
}; };
public updateColumnOptions = (): void => { public updateColumnOptions = (): void => {
let originalHeaders = this.columnOptions(); // let originalHeaders = this.columnOptions();
let newHeaders = this.tableEntityListViewModel.headers; const newHeaders = this.tableEntityListViewModel.headers;
this.columnOptions(newHeaders.sort(DataTableUtilities.compareTableColumns)); this.columnOptions(newHeaders.sort(DataTableUtilities.compareTableColumns));
}; };
private generateLeftParentheses(clause: QueryClauseViewModel): string { private generateLeftParentheses(clause: QueryClauseViewModel): string {
var result = ""; let result = "";
if (clause.clauseGroup.isRootGroup || clause.clauseGroup.children.indexOf(clause) !== 0) { if (clause.clauseGroup.isRootGroup || clause.clauseGroup.children.indexOf(clause) !== 0) {
return result; return result;
@@ -307,7 +307,7 @@ export default class QueryBuilderViewModel {
result = result.concat("("); result = result.concat("(");
} }
var currentGroup: ClauseGroup = clause.clauseGroup; let currentGroup: ClauseGroup = clause.clauseGroup;
while ( while (
!currentGroup.isRootGroup && !currentGroup.isRootGroup &&
@@ -322,7 +322,7 @@ export default class QueryBuilderViewModel {
} }
private generateRightParentheses(clause: QueryClauseViewModel): string { private generateRightParentheses(clause: QueryClauseViewModel): string {
var result = ""; let result = "";
if ( if (
clause.clauseGroup.isRootGroup || clause.clauseGroup.isRootGroup ||
@@ -333,7 +333,7 @@ export default class QueryBuilderViewModel {
result = result.concat(")"); result = result.concat(")");
} }
var currentGroup: ClauseGroup = clause.clauseGroup; let currentGroup: ClauseGroup = clause.clauseGroup;
while ( while (
!currentGroup.isRootGroup && !currentGroup.isRootGroup &&
@@ -364,14 +364,17 @@ export default class QueryBuilderViewModel {
case Constants.TableType.String: case Constants.TableType.String:
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
operator operator
// eslint-disable-next-line no-useless-escape
)} \'${value}\'${rightParentheses}`; )} \'${value}\'${rightParentheses}`;
case Constants.TableType.Guid: case Constants.TableType.Guid:
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
operator operator
// eslint-disable-next-line no-useless-escape
)} guid\'${value}\'${rightParentheses}`; )} guid\'${value}\'${rightParentheses}`;
case Constants.TableType.Binary: case Constants.TableType.Binary:
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
operator operator
// eslint-disable-next-line no-useless-escape
)} binary\'${value}\'${rightParentheses}`; )} binary\'${value}\'${rightParentheses}`;
default: default:
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter( return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
@@ -391,9 +394,11 @@ export default class QueryBuilderViewModel {
): string => { ): string => {
if (propertyName === Constants.EntityKeyNames.PartitionKey) { if (propertyName === Constants.EntityKeyNames.PartitionKey) {
propertyName = TableEntityProcessor.keyProperties.PartitionKey; propertyName = TableEntityProcessor.keyProperties.PartitionKey;
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c["${propertyName}"] ${operator} \'${value}\'${rightParentheses}`; return ` ${clauseRule.toLowerCase()} ${leftParentheses}c["${propertyName}"] ${operator} \'${value}\'${rightParentheses}`;
} else if (propertyName === Constants.EntityKeyNames.RowKey) { } else if (propertyName === Constants.EntityKeyNames.RowKey) {
propertyName = TableEntityProcessor.keyProperties.Id; propertyName = TableEntityProcessor.keyProperties.Id;
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName} ${operator} \'${value}\'${rightParentheses}`; return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName} ${operator} \'${value}\'${rightParentheses}`;
} else if (propertyName === Constants.EntityKeyNames.Timestamp) { } else if (propertyName === Constants.EntityKeyNames.Timestamp) {
propertyName = TableEntityProcessor.keyProperties.Timestamp; propertyName = TableEntityProcessor.keyProperties.Timestamp;
@@ -403,16 +408,21 @@ export default class QueryBuilderViewModel {
} }
switch (type) { switch (type) {
case Constants.TableType.DateTime: case Constants.TableType.DateTime:
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${DateTimeUtilities.convertJSDateToTicksWithPadding( return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${DateTimeUtilities.convertJSDateToTicksWithPadding(
value value
// eslint-disable-next-line no-useless-escape
)}\'${rightParentheses}`; )}\'${rightParentheses}`;
case Constants.TableType.Int64: case Constants.TableType.Int64:
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${Utilities.padLongWithZeros( return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${Utilities.padLongWithZeros(
value value
// eslint-disable-next-line no-useless-escape
)}\'${rightParentheses}`; )}\'${rightParentheses}`;
case Constants.TableType.String: case Constants.TableType.String:
case Constants.TableType.Guid: case Constants.TableType.Guid:
case Constants.TableType.Binary: case Constants.TableType.Binary:
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${value}\'${rightParentheses}`; return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${value}\'${rightParentheses}`;
default: default:
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} ${value}${rightParentheses}`; return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} ${value}${rightParentheses}`;
@@ -434,6 +444,7 @@ export default class QueryBuilderViewModel {
type === Constants.CassandraType.Ascii || type === Constants.CassandraType.Ascii ||
type === Constants.CassandraType.Varchar type === Constants.CassandraType.Varchar
) { ) {
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} \'${value}\'${rightParentheses}`; return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} \'${value}\'${rightParentheses}`;
} }
return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} ${value}${rightParentheses}`; return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} ${value}${rightParentheses}`;
@@ -454,7 +465,7 @@ export default class QueryBuilderViewModel {
case Constants.Operator.NotEqualTo: case Constants.Operator.NotEqualTo:
return Constants.ODataOperator.NotEqualTo; return Constants.ODataOperator.NotEqualTo;
} }
return null; return undefined;
}; };
public groupClauses = (): void => { public groupClauses = (): void => {
@@ -463,11 +474,11 @@ export default class QueryBuilderViewModel {
this.updateCanGroupClauses(); this.updateCanGroupClauses();
}; };
public addClauseIndex = (index: number, data: any): void => { public addClauseIndex = (index: number): void => {
if (index < 0) { if (index < 0) {
index = 0; index = 0;
} }
var newClause = new QueryClauseViewModel( const newClause = new QueryClauseViewModel(
this, this,
"And", "And",
"", "",
@@ -492,28 +503,28 @@ export default class QueryBuilderViewModel {
// adds a new clause to the end of the array // adds a new clause to the end of the array
public addNewClause = (): void => { public addNewClause = (): void => {
this.addClauseIndex(this.clauseArray().length, null); this.addClauseIndex(this.clauseArray().length);
}; };
public onAddClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => { public onAddClauseKeyDown = (index: number, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.addClauseIndex(index, data); this.addClauseIndex(index);
event.stopPropagation(); event.stopPropagation();
return false; return false;
} }
return true; return true;
}; };
public onAddNewClauseKeyDown = (source: any, event: KeyboardEvent): boolean => { public onAddNewClauseKeyDown = (event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.addClauseIndex(this.clauseArray().length - 1, null); this.addClauseIndex(this.clauseArray().length - 1);
event.stopPropagation(); event.stopPropagation();
return false; return false;
} }
return true; return true;
}; };
public deleteClause = (index: number, data: any): void => { public deleteClause = (index: number): void => {
this.deleteClauseImpl(index); this.deleteClauseImpl(index);
if (this.clauseArray().length !== 0) { if (this.clauseArray().length !== 0) {
this.clauseArray()[0].and_or(""); this.clauseArray()[0].and_or("");
@@ -523,9 +534,9 @@ export default class QueryBuilderViewModel {
$(window).resize(); $(window).resize();
}; };
public onDeleteClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => { public onDeleteClauseKeyDown = (index: number, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.deleteClause(index, data); this.deleteClause(index);
event.stopPropagation(); event.stopPropagation();
return false; return false;
} }
@@ -539,25 +550,26 @@ export default class QueryBuilderViewModel {
* (transparent) or its parent group view models. * (transparent) or its parent group view models.
*/ */
public getClauseGroupViewModels = (clause: QueryClauseViewModel): ClauseGroupViewModel[] => { public getClauseGroupViewModels = (clause: QueryClauseViewModel): ClauseGroupViewModel[] => {
var placeHolderGroupViewModel = new ClauseGroupViewModel(this.queryClauses, false, this); const placeHolderGroupViewModel = new ClauseGroupViewModel(this.queryClauses, false, this);
var treeDepth = this.queryClauses.getTreeDepth(); const treeDepth = this.queryClauses.getTreeDepth();
var groupViewModels = new Array<ClauseGroupViewModel>(treeDepth); const groupViewModels = new Array<ClauseGroupViewModel>(treeDepth);
// Prefill the arry with placeholders. // Prefill the arry with placeholders.
for (var i = 0; i < groupViewModels.length; i++) { for (let i = 0; i < groupViewModels.length; i++) {
groupViewModels[i] = placeHolderGroupViewModel; groupViewModels[i] = placeHolderGroupViewModel;
} }
var currentGroup = clause.clauseGroup; let currentGroup = clause.clauseGroup;
// This function determines whether the path from clause to the current group is on the left most. // This function determines whether the path from clause to the current group is on the left most.
var isLeftMostPath = (): boolean => { const isLeftMostPath = (): boolean => {
var group = clause.clauseGroup; let group = clause.clauseGroup;
if (group.children.indexOf(clause) !== 0) { if (group.children.indexOf(clause) !== 0) {
return false; return false;
} }
// eslint-disable-next-line no-constant-condition
while (true) { while (true) {
if (group.getId() === currentGroup.getId()) { if (group.getId() === currentGroup.getId()) {
break; break;
@@ -573,13 +585,14 @@ export default class QueryBuilderViewModel {
}; };
// This function determines whether the path from clause to the current group is on the right most. // This function determines whether the path from clause to the current group is on the right most.
var isRightMostPath = (): boolean => { const isRightMostPath = (): boolean => {
var group = clause.clauseGroup; let group = clause.clauseGroup;
if (group.children.indexOf(clause) !== group.children.length - 1) { if (group.children.indexOf(clause) !== group.children.length - 1) {
return false; return false;
} }
// eslint-disable-next-line no-constant-condition
while (true) { while (true) {
if (group.getId() === currentGroup.getId()) { if (group.getId() === currentGroup.getId()) {
break; break;
@@ -594,26 +607,26 @@ export default class QueryBuilderViewModel {
return true; return true;
}; };
var vmIndex = groupViewModels.length - 1; let vmIndex = groupViewModels.length - 1;
var skipIndex = -1; let skipIndex = -1;
var lastDepth = clause.groupDepth; let lastDepth = clause.groupDepth;
while (!currentGroup.isRootGroup) { while (!currentGroup.isRootGroup) {
// The current group will be rendered at least once, and if there are any sibling groups deeper // The current group will be rendered at least once, and if there are any sibling groups deeper
// than the current group, we will repeat rendering the current group to fill up the gap between // than the current group, we will repeat rendering the current group to fill up the gap between
// current & deepest sibling. // current & deepest sibling.
var deepestInSiblings = currentGroup.findDeepestGroupInChildren(skipIndex).getCurrentGroupDepth(); const deepestInSiblings = currentGroup.findDeepestGroupInChildren(skipIndex).getCurrentGroupDepth();
// Find out the depth difference between the deepest group under the siblings of currentGroup and // Find out the depth difference between the deepest group under the siblings of currentGroup and
// the deepest group under currentGroup. If the result n is a positive number, it means there are // the deepest group under currentGroup. If the result n is a positive number, it means there are
// deeper groups in siblings and we need to draw n + 1 group blocks on UI to fill up the depth // deeper groups in siblings and we need to draw n + 1 group blocks on UI to fill up the depth
// differences. If the result n is a negative number, it means current group contains the deepest // differences. If the result n is a negative number, it means current group contains the deepest
// sub-group, we only need to draw the group block once. // sub-group, we only need to draw the group block once.
var repeatCount = Math.max(deepestInSiblings - lastDepth, 0); const repeatCount = Math.max(deepestInSiblings - lastDepth, 0);
for (var i = 0; i <= repeatCount; i++) { for (let i = 0; i <= repeatCount; i++) {
var isLeftMost = isLeftMostPath(); const isLeftMost = isLeftMostPath();
var isRightMost = isRightMostPath(); const isRightMost = isRightMostPath();
var groupViewModel = new ClauseGroupViewModel(currentGroup, i === 0 && isLeftMost, this); const groupViewModel = new ClauseGroupViewModel(currentGroup, i === 0 && isLeftMost, this);
groupViewModel.showTopBorder(isLeftMost); groupViewModel.showTopBorder(isLeftMost);
groupViewModel.showBottomBorder(isRightMost); groupViewModel.showBottomBorder(isRightMost);
@@ -635,9 +648,9 @@ export default class QueryBuilderViewModel {
}; };
public addCustomRange(timestamp: CustomTimestampHelper.ITimestampQuery, clauseToAdd: QueryClauseViewModel): void { public addCustomRange(timestamp: CustomTimestampHelper.ITimestampQuery, clauseToAdd: QueryClauseViewModel): void {
var index = this.clauseArray.peek().indexOf(clauseToAdd); const index = this.clauseArray.peek().indexOf(clauseToAdd);
var newClause = new QueryClauseViewModel( const newClause = new QueryClauseViewModel(
this, this,
//this._tableEntityListViewModel.tableExplorerContext.hostProxy, //this._tableEntityListViewModel.tableExplorerContext.hostProxy,
"And", "And",
@@ -662,10 +675,10 @@ export default class QueryBuilderViewModel {
} }
private scrollToBottom(): void { private scrollToBottom(): void {
var scrollBox = document.getElementById("scroll"); const scrollBox = document.getElementById("scroll");
if (!this.scrollEventListener) { if (!this.scrollEventListener) {
scrollBox.addEventListener("scroll", function () { scrollBox.addEventListener("scroll", function () {
var translate = "translate(0," + this.scrollTop + "px)"; const translate = "translate(0," + this.scrollTop + "px)";
const allTh = <NodeListOf<HTMLElement>>this.querySelectorAll("thead td"); const allTh = <NodeListOf<HTMLElement>>this.querySelectorAll("thead td");
for (let i = 0; i < allTh.length; i++) { for (let i = 0; i < allTh.length; i++) {
allTh[i].style.transform = translate; allTh[i].style.transform = translate;
@@ -673,7 +686,7 @@ export default class QueryBuilderViewModel {
}); });
this.scrollEventListener = true; this.scrollEventListener = true;
} }
var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1; const isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1;
if (isScrolledToBottom) { if (isScrolledToBottom) {
scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight; scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight;
} }
@@ -685,8 +698,8 @@ export default class QueryBuilderViewModel {
} }
private deleteClauseImpl(index: number): void { private deleteClauseImpl(index: number): void {
var clause = this.clauseArray()[index]; const clause = this.clauseArray()[index];
var previousClause = index === 0 ? 0 : index - 1; const previousClause = index === 0 ? 0 : index - 1;
this.queryClauses.deleteClause(clause); this.queryClauses.deleteClause(clause);
this.updateClauseArray(); this.updateClauseArray();
if (this.clauseArray()[previousClause]) { if (this.clauseArray()[previousClause]) {
@@ -731,7 +744,7 @@ export default class QueryBuilderViewModel {
private timestampToSqlValue(clause: QueryClauseViewModel): string { private timestampToSqlValue(clause: QueryClauseViewModel): string {
if (clause.isValue()) { if (clause.isValue()) {
return null; return undefined;
} else if (clause.isTimestamp()) { } else if (clause.isTimestamp()) {
return this.getTimeStampToSqlQuery(clause); return this.getTimeStampToSqlQuery(clause);
// } else if (clause.isCustomLastTimestamp()) { // } else if (clause.isCustomLastTimestamp()) {
@@ -743,7 +756,7 @@ export default class QueryBuilderViewModel {
return clause.customTimeValue(); return clause.customTimeValue();
} }
} }
return null; return undefined;
} }
private getTimeStampToQuery(clause: QueryClauseViewModel): void { private getTimeStampToQuery(clause: QueryClauseViewModel): void {
@@ -789,7 +802,7 @@ export default class QueryBuilderViewModel {
case Constants.timeOptions.currentYear: case Constants.timeOptions.currentYear:
return CustomTimestampHelper._queryCurrentYearLocal(); return CustomTimestampHelper._queryCurrentYearLocal();
} }
return null; return undefined;
} }
public checkIfClauseChanged(): void { public checkIfClauseChanged(): void {

View File

@@ -14,7 +14,7 @@ export default class QueryClauseViewModel {
public field: ko.Observable<string>; public field: ko.Observable<string>;
public type: ko.Observable<string>; public type: ko.Observable<string>;
public operator: ko.Observable<string>; public operator: ko.Observable<string>;
public value: ko.Observable<any>; public value: ko.Observable<string>;
public timeValue: ko.Observable<string>; public timeValue: ko.Observable<string>;
public customTimeValue: ko.Observable<string>; public customTimeValue: ko.Observable<string>;
public canAnd: ko.Observable<boolean>; public canAnd: ko.Observable<boolean>;
@@ -39,7 +39,7 @@ export default class QueryClauseViewModel {
field: string, field: string,
type: string, type: string,
operator: string, operator: string,
value: any, value: string,
canAnd: boolean, canAnd: boolean,
timeValue: string, timeValue: string,
customTimeValue: string, customTimeValue: string,
@@ -88,30 +88,30 @@ export default class QueryClauseViewModel {
userContext.apiType !== "Cassandra" userContext.apiType !== "Cassandra"
); );
this.and_or.subscribe((value) => { this.and_or.subscribe(() => {
this._queryBuilderViewModel.checkIfClauseChanged(); this._queryBuilderViewModel.checkIfClauseChanged();
}); });
this.field.subscribe((value) => { this.field.subscribe(() => {
this.changeField(); this.changeField();
}); });
this.type.subscribe((value) => { this.type.subscribe(() => {
this.changeType(); this.changeType();
}); });
this.timeValue.subscribe((value) => { this.timeValue.subscribe(() => {
// if (this.timeValue() === QueryBuilderConstants.timeOptions.custom) { // if (this.timeValue() === QueryBuilderConstants.timeOptions.custom) {
// this.customTimestampDialog(); // this.customTimestampDialog();
// } // }
}); });
this.customTimeValue.subscribe((value) => { this.customTimeValue.subscribe(() => {
this._queryBuilderViewModel.checkIfClauseChanged(); this._queryBuilderViewModel.checkIfClauseChanged();
}); });
this.value.subscribe((value) => { this.value.subscribe(() => {
this._queryBuilderViewModel.checkIfClauseChanged(); this._queryBuilderViewModel.checkIfClauseChanged();
}); });
this.operator.subscribe((value) => { this.operator.subscribe(() => {
this._queryBuilderViewModel.checkIfClauseChanged(); this._queryBuilderViewModel.checkIfClauseChanged();
}); });
this._groupCheckSubscription = this.checkedForGrouping.subscribe((value) => { this._groupCheckSubscription = this.checkedForGrouping.subscribe(() => {
this._queryBuilderViewModel.updateCanGroupClauses(); this._queryBuilderViewModel.updateCanGroupClauses();
}); });
this.isAndOrFocused = ko.observable<boolean>(false); this.isAndOrFocused = ko.observable<boolean>(false);
@@ -280,7 +280,7 @@ export default class QueryClauseViewModel {
this._groupCheckSubscription.dispose(); this._groupCheckSubscription.dispose();
} }
this.clauseGroup = null; this.clauseGroup = undefined;
this._queryBuilderViewModel = null; this._queryBuilderViewModel = undefined;
} }
} }

View File

@@ -202,14 +202,21 @@ 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;
for (let 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()
) { ) {
updateQuery += this.isStringType(newEntity[property].$) let propertyQuerySegment = this.isStringType(newEntity[property].$)
? ` SET ${property} = '${newEntity[property]._}',` ? `${property} = '${newEntity[property]._}',`
: ` SET ${property} = ${newEntity[property]._},`; : `${property} = ${newEntity[property]._},`;
// Only add the "SET" keyword once
if (isFirstPropertyToUpdate) {
propertyQuerySegment = " SET " + propertyQuerySegment;
isFirstPropertyToUpdate = false;
}
updateQuery += propertyQuerySegment;
isPropertyUpdated = true; isPropertyUpdated = true;
} }
} }

View File

@@ -17,6 +17,7 @@ export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
/** /**
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton) * 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,

View File

@@ -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(
@@ -118,7 +120,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
const saveButtonChildren = []; const saveButtonChildren = [];
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
saveButtonChildren.push({ saveButtonChildren.push({
iconName: "Copy", iconName: copyToLabel,
onCommandClick: () => this.copyNotebook(), onCommandClick: () => this.copyNotebook(),
commandButtonLabel: copyToLabel, commandButtonLabel: copyToLabel,
hasPopup: false, hasPopup: false,

View File

@@ -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,
}; };
} }
} }

View File

@@ -1,4 +1,5 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { useNotebook } from "Explorer/Notebook/useNotebook";
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 (useNotebook.getState().isPhoenix) {
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,9 @@ 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(); userContext.databaseAccount?.properties.capacity?.totalThroughputLimit
? 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",

View File

@@ -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,10 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) {
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());

View File

@@ -121,14 +121,18 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
children: [], children: [],
}; };
if (userContext.features.notebooksTemporarilyDown) { if (useNotebook.getState().isPhoenix === false && userContext.features.notebooksDownBanner) {
notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} else { } else if (useNotebook.getState().isPhoenix) {
if (galleryContentRoot) { if (galleryContentRoot) {
notebooksTree.children.push(buildGalleryNotebooksTree()); notebooksTree.children.push(buildGalleryNotebooksTree());
} }
if (myNotebooksContentRoot && useNotebook.getState().connectionInfo.status == ConnectionStatusType.Connected) { if (
myNotebooksContentRoot &&
useNotebook.getState().isPhoenix &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
) {
notebooksTree.children.push(buildMyNotebooksTree()); notebooksTree.children.push(buildMyNotebooksTree());
} }
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
@@ -161,11 +165,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode( const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
myNotebooksContentRoot, myNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => { container.openNotebook(item);
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
} }
); );
@@ -180,11 +180,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot, gitHubNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => { container.openNotebook(item);
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
}, },
true true
); );
@@ -213,23 +209,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}, },
}, },
]; ];
const connectGitContextMenu: TreeNodeMenuItem[] = [ gitHubNotebooksTree.contextMenu = manageGitContextMenu;
{
label: "Connect to GitHub",
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Connect to GitHub",
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={container.notebookManager.junoClient}
/>
),
},
];
gitHubNotebooksTree.contextMenu = isConnected ? manageGitContextMenu : connectGitContextMenu;
gitHubNotebooksTree.isExpanded = true; gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true; gitHubNotebooksTree.isAlphaSorted = true;
@@ -531,7 +511,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
isNotebookEnabled && isNotebookEnabled &&
userContext.apiType === "Mongo" && userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed() && isPublicInternetAccessAllowed() &&
!userContext.features.notebooksTemporarilyDown useNotebook.getState().isPhoenix
) { ) {
children.push({ children.push({
label: "Schema (Preview)", label: "Schema (Preview)",

View File

@@ -45,7 +45,6 @@ import UserDefinedFunction from "./UserDefinedFunction";
export class ResourceTreeAdapter implements ReactAdapter { export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks"; public static readonly MyNotebooksTitle = "My Notebooks";
public static readonly MyNotebooksScratchTitle = "My Notebooks Scratch";
public static readonly GitHubReposTitle = "GitHub repos"; public static readonly GitHubReposTitle = "GitHub repos";
private static readonly DataTitle = "DATA"; private static readonly DataTitle = "DATA";

View File

@@ -22,6 +22,7 @@ export default class Trigger {
public triggerType: ko.Observable<string>; public triggerType: ko.Observable<string>;
public triggerOperation: ko.Observable<string>; public triggerOperation: ko.Observable<string>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(container: Explorer, collection: ViewModels.Collection, data: any) { constructor(container: Explorer, collection: ViewModels.Collection, data: any) {
this.nodeKind = "Trigger"; this.nodeKind = "Trigger";
this.container = container; this.container = container;
@@ -34,7 +35,7 @@ export default class Trigger {
this.triggerType = ko.observable(data.triggerType); this.triggerType = ko.observable(data.triggerType);
} }
public select() { public select(): void {
useSelectedNode.getState().setSelectedNode(this); useSelectedNode.getState().setSelectedNode(this);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Trigger node", description: "Trigger node",
@@ -43,7 +44,8 @@ export default class Trigger {
}); });
} }
public static create(source: ViewModels.Collection, event: MouseEvent) { // eslint-disable-next-line @typescript-eslint/no-unused-vars
public static create(source: ViewModels.Collection, _event: MouseEvent): void {
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Triggers).length + 1; const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Triggers).length + 1;
const trigger = <StoredProcedureDefinition>{ const trigger = <StoredProcedureDefinition>{
id: "", id: "",
@@ -99,7 +101,7 @@ export default class Trigger {
} }
}; };
public delete() { public delete(): void {
useDialog.getState().showOkCancelModalDialog( useDialog.getState().showOkCancelModalDialog(
"Confirm delete", "Confirm delete",
"Are you sure you want to delete the trigger?", "Are you sure you want to delete the trigger?",
@@ -110,7 +112,8 @@ export default class Trigger {
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this); this.collection.children.remove(this);
}, },
(reason) => {} // eslint-disable-next-line @typescript-eslint/no-empty-function
() => {}
); );
}, },
"Cancel", "Cancel",

View File

@@ -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;

View File

@@ -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;
},
})); }));

View File

@@ -64,7 +64,7 @@ export class GitHubOAuthService {
return params.state; return params.state;
} }
public async finishOAuth(params: IGitHubConnectorParams) { public async finishOAuth(params: IGitHubConnectorParams): Promise<void> {
try { try {
this.validateState(params.state); this.validateState(params.state);
const response = await this.junoClient.getGitHubToken(params.code); const response = await this.junoClient.getGitHubToken(params.code);
@@ -113,7 +113,7 @@ export class GitHubOAuthService {
return this.state; return this.state;
} }
public resetToken() { public resetToken(): void {
this.token(undefined); this.token(undefined);
} }

View File

@@ -50,7 +50,8 @@ describe("Pinned repos", () => {
}); });
it("updatePinnedRepos invokes pinned repos subscribers", async () => { it("updatePinnedRepos invokes pinned repos subscribers", async () => {
const callback = jest.fn().mockImplementation((pinnedRepos: IPinnedRepo[]) => {}); // eslint-disable-next-line @typescript-eslint/no-empty-function
const callback = jest.fn().mockImplementation(() => {});
junoClient.subscribeToPinnedRepos(callback); junoClient.subscribeToPinnedRepos(callback);
const response = await junoClient.updatePinnedRepos(samplePinnedRepos); const response = await junoClient.updatePinnedRepos(samplePinnedRepos);
@@ -60,7 +61,8 @@ describe("Pinned repos", () => {
}); });
it("getPinnedRepos invokes pinned repos subscribers", async () => { it("getPinnedRepos invokes pinned repos subscribers", async () => {
const callback = jest.fn().mockImplementation((pinnedRepos: IPinnedRepo[]) => {}); // eslint-disable-next-line @typescript-eslint/no-empty-function
const callback = jest.fn().mockImplementation(() => {});
junoClient.subscribeToPinnedRepos(callback); junoClient.subscribeToPinnedRepos(callback);
const response = await junoClient.getPinnedRepos("scope"); const response = await junoClient.getPinnedRepos("scope");
@@ -153,7 +155,7 @@ describe("Gallery", () => {
it("getSampleNotebooks", async () => { it("getSampleNotebooks", async () => {
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.getSampleNotebooks(); const response = await junoClient.getSampleNotebooks();
@@ -165,7 +167,7 @@ describe("Gallery", () => {
it("getPublicNotebooks", async () => { it("getPublicNotebooks", async () => {
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.getPublicNotebooks(); const response = await junoClient.getPublicNotebooks();
@@ -178,7 +180,7 @@ describe("Gallery", () => {
const id = "id"; const id = "id";
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.getNotebookInfo(id); const response = await junoClient.getNotebookInfo(id);
@@ -191,7 +193,7 @@ describe("Gallery", () => {
const id = "id"; const id = "id";
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
text: () => undefined as any, text: () => undefined as undefined,
}); });
const response = await junoClient.getNotebookContent(id); const response = await junoClient.getNotebookContent(id);
@@ -204,7 +206,7 @@ describe("Gallery", () => {
const id = "id"; const id = "id";
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.increaseNotebookViews(id); const response = await junoClient.increaseNotebookViews(id);
@@ -218,7 +220,7 @@ describe("Gallery", () => {
const id = "id"; const id = "id";
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.increaseNotebookDownloadCount(id); const response = await junoClient.increaseNotebookDownloadCount(id);
@@ -243,7 +245,7 @@ describe("Gallery", () => {
const id = "id"; const id = "id";
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.favoriteNotebook(id); const response = await junoClient.favoriteNotebook(id);
@@ -268,7 +270,7 @@ describe("Gallery", () => {
const id = "id"; const id = "id";
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.unfavoriteNotebook(id); const response = await junoClient.unfavoriteNotebook(id);
@@ -292,7 +294,7 @@ describe("Gallery", () => {
it("getFavoriteNotebooks", async () => { it("getFavoriteNotebooks", async () => {
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.getFavoriteNotebooks(); const response = await junoClient.getFavoriteNotebooks();
@@ -315,7 +317,7 @@ describe("Gallery", () => {
it("getPublishedNotebooks", async () => { it("getPublishedNotebooks", async () => {
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.getPublishedNotebooks(); const response = await junoClient.getPublishedNotebooks();
@@ -339,7 +341,7 @@ describe("Gallery", () => {
const id = "id"; const id = "id";
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.deleteNotebook(id); const response = await junoClient.deleteNotebook(id);
@@ -369,7 +371,7 @@ describe("Gallery", () => {
const addLinkToNotebookViewer = true; const addLinkToNotebookViewer = true;
window.fetch = jest.fn().mockReturnValue({ window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
json: () => undefined as any, json: () => undefined as undefined,
}); });
const response = await junoClient.publishNotebook(name, description, tags, thumbnailUrl, content); const response = await junoClient.publishNotebook(name, description, tags, thumbnailUrl, content);

View File

@@ -62,7 +62,7 @@ export interface IPublishNotebookRequest {
description: string; description: string;
tags: string[]; tags: string[];
thumbnailUrl: string; thumbnailUrl: string;
content: any; content: unknown;
addLinkToNotebookViewer: boolean; addLinkToNotebookViewer: boolean;
} }

View File

@@ -1,68 +1,160 @@
import { ConnectionStatusType, 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 { ContainerConnectionInfo } from "../Contracts/DataModels"; import {
ContainerInfo,
IAccountData,
IContainerData,
IPhoenixConnectionInfoResult,
IProvisionData,
IResponse,
} from "../Contracts/DataModels";
import { useNotebook } from "../Explorer/Notebook/useNotebook"; 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;
resourceId: 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,
accountData: IAccountData
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
return this.executeContainerAssignmentOperation(provisionData, accountData, "allocate");
}
public async resetContainer(
provisionData: IProvisionData,
accountData: IAccountData
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
return this.executeContainerAssignmentOperation(provisionData, accountData, "reset");
}
private async executeContainerAssignmentOperation(
provisionData: IProvisionData,
accountData: IAccountData,
operation: string
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
try { try {
const connectionStatus: ContainerConnectionInfo = { const response = await fetch(
status: ConnectionStatusType.Connecting, `${this.getPhoenixContainerPoolingEndPoint()}/subscriptions/${accountData.subscriptionId}/resourceGroups/${
}; accountData.resourceGroup
useNotebook.getState().setConnectionInfo(connectionStatus); }/providers/Microsoft.DocumentDB/databaseAccounts/${accountData.dbAccountName}/containerconnections`,
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, { {
method: "POST", method: operation === "allocate" ? "POST" : "PATCH",
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();
if (data && data.notebookServerUrl) {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
}
} else {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().setConnectionInfo(connectionStatus);
} }
return { return {
status: response.status, status: response.status,
data, data,
}; };
} catch (error) { } catch (error) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
console.error(error); console.error(error);
throw error; throw error;
} }
} }
public async initiateContainerHeartBeat(containerData: IContainerData) {
if (this.containerHealthHandler) {
clearTimeout(this.containerHealthHandler);
}
await this.getContainerHealth(Notebook.containerStatusHeartbeatDelayMs, containerData);
}
private scheduleContainerHeartbeat(delayMs: number, containerData: IContainerData): void {
this.containerHealthHandler = setTimeout(async () => {
await this.getContainerHealth(delayMs, containerData);
}, delayMs);
}
private async getContainerStatusAsync(containerData: IContainerData): Promise<ContainerInfo> {
try {
const runContainerStatusAsync = async () => {
const response = await window.fetch(
`${this.getPhoenixContainerPoolingEndPoint()}/subscriptions/${containerData.subscriptionId}/resourceGroups/${
containerData.resourceGroup
}/providers/Microsoft.DocumentDB/databaseAccounts/${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: IContainerData) {
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 async IsDbAcountWhitelisted(accountData: IAccountData) {
try {
const response = await window.fetch(
`${this.getPhoenixContainerPoolingEndPoint()}/subscriptions/${accountData.subscriptionId}/resourceGroups/${
accountData.resourceGroup
}/providers/Microsoft.DocumentDB/databaseAccounts/${accountData.dbAccountName}`,
{
method: "GET",
headers: PhoenixClient.getHeaders(),
}
);
return response.status === HttpStatusCodes.OK;
} catch (error) {
Logger.logError(getErrorMessage(error), "PhoenixClient/IsDbAcountWhitelisted");
return false;
}
}
public static getPhoenixEndpoint(): string { public static getPhoenixEndpoint(): string {
const phoenixEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; const phoenixEndpoint =
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) { if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
const error = `${phoenixEndpoint} not allowed as juno endpoint`; const error = `${phoenixEndpoint} not allowed as juno endpoint`;
console.error(error); console.error(error);
@@ -73,8 +165,9 @@ export class PhoenixClient {
} }
public getPhoenixContainerPoolingEndPoint(): string { public getPhoenixContainerPoolingEndPoint(): string {
return `${PhoenixClient.getPhoenixEndpoint()}/api/containerpooling`; return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer/cosmosaccounts`;
} }
private static getHeaders(): HeadersInit { private static getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
return { return {

View File

@@ -11,7 +11,7 @@ export type Features = {
autoscaleDefault: boolean; autoscaleDefault: boolean;
partitionKeyDefault: boolean; partitionKeyDefault: boolean;
partitionKeyDefault2: boolean; partitionKeyDefault2: boolean;
phoenix: boolean; notebooksDownBanner: boolean;
readonly enableSDKoperations: boolean; readonly enableSDKoperations: boolean;
readonly enableSpark: boolean; readonly enableSpark: boolean;
readonly enableTtl: boolean; readonly enableTtl: boolean;
@@ -20,6 +20,7 @@ export type Features = {
readonly enableKoResourceTree: boolean; readonly enableKoResourceTree: boolean;
readonly hostedDataExplorer: boolean; readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string; readonly junoEndpoint?: string;
readonly phoenixEndpoint?: string;
readonly livyEndpoint?: string; readonly livyEndpoint?: string;
readonly notebookBasePath?: string; readonly notebookBasePath?: string;
readonly notebookServerToken?: string; readonly notebookServerToken?: string;
@@ -31,7 +32,7 @@ export type Features = {
readonly ttl90Days: boolean; readonly ttl90Days: boolean;
readonly mongoProxyEndpoint?: string; readonly mongoProxyEndpoint?: string;
readonly mongoProxyAPIs?: string; readonly mongoProxyAPIs?: string;
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 {
@@ -68,6 +69,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
mongoProxyEndpoint: get("mongoproxyendpoint"), mongoProxyEndpoint: get("mongoproxyendpoint"),
mongoProxyAPIs: get("mongoproxyapis"), mongoProxyAPIs: get("mongoproxyapis"),
junoEndpoint: get("junoendpoint"), junoEndpoint: get("junoendpoint"),
phoenixEndpoint: get("phoenixendpoint"),
livyEndpoint: get("livyendpoint"), livyEndpoint: get("livyendpoint"),
notebookBasePath: get("notebookbasepath"), notebookBasePath: get("notebookbasepath"),
notebookServerToken: get("notebookservertoken"), notebookServerToken: get("notebookservertoken"),
@@ -80,8 +82,8 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
autoscaleDefault: "true" === get("autoscaledefault"), autoscaleDefault: "true" === get("autoscaledefault"),
partitionKeyDefault: "true" === get("partitionkeytest"), partitionKeyDefault: "true" === get("partitionkeytest"),
partitionKeyDefault2: "true" === get("pkpartitionkeytest"), partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"), notebooksDownBanner: "true" === get("notebooksDownBanner"),
phoenix: "true" === get("phoenix"), enableThroughputCap: "true" === get("enablethroughputcap"),
}; };
} }

View File

@@ -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;
} }
}; };

View File

@@ -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") {

View File

@@ -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 = {

View File

@@ -30,7 +30,7 @@ describe("GalleryUtils", () => {
}); });
it("downloadItem shows dialog in data explorer", () => { it("downloadItem shows dialog in data explorer", () => {
const container = {} as Explorer; const container = new Explorer();
GalleryUtils.downloadItem(container, undefined, galleryItem, undefined); GalleryUtils.downloadItem(container, undefined, galleryItem, undefined);
expect(useDialog.getState().visible).toBe(true); expect(useDialog.getState().visible).toBe(true);

View File

@@ -225,67 +225,89 @@ export function downloadItem(
const name = data.name; const name = data.name;
useDialog.getState().showOkCancelModalDialog( useDialog.getState().showOkCancelModalDialog(
`Download to ${useNotebook.getState().notebookFolderName}`, `Download to ${useNotebook.getState().notebookFolderName}`,
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, undefined,
"Download", "Download",
async () => { async () => {
const clearInProgressMessage = logConsoleProgress( if (useNotebook.getState().isPhoenix) {
`Downloading ${name} to ${useNotebook.getState().notebookFolderName}` await container.allocateContainer();
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
downloadNotebookItem(name, data, junoClient, container, onComplete);
} else {
useDialog
.getState()
.showOkModalDialog(
"Failed to connect",
"Failed to connect to temporary workspace. Please refresh the page and try again."
);
}
},
"Cancel",
undefined,
container.getDownloadModalConent(name)
);
}
export async function downloadNotebookItem(
fileName: string,
data: IGalleryItem,
junoClient: JunoClient,
container: Explorer,
onComplete: (item: IGalleryItem) => void
) {
const clearInProgressMessage = logConsoleProgress(
`Downloading ${fileName} to ${useNotebook.getState().notebookFolderName}`
);
const startKey = traceStart(Action.NotebooksGalleryDownload, {
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
});
try {
const response = await junoClient.getNotebookContent(data.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
}
const notebook = JSON.parse(response.data) as Notebook;
removeNotebookViewerLink(notebook, data.newCellId);
if (!data.isSample) {
const metadata = notebook.metadata as { [name: string]: unknown };
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`);
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
if (increaseDownloadResponse.data) {
traceSuccess(
Action.NotebooksGalleryDownload,
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
startKey
); );
const startKey = traceStart(Action.NotebooksGalleryDownload, { onComplete(increaseDownloadResponse.data);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryDownload,
{
notebookId: data.id, notebookId: data.id,
downloadCount: data.downloads, downloadCount: data.downloads,
isSample: data.isSample, isSample: data.isSample,
}); error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
try { handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
const response = await junoClient.getNotebookContent(data.id); }
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
}
const notebook = JSON.parse(response.data) as Notebook; clearInProgressMessage();
removeNotebookViewerLink(notebook, data.newCellId);
if (!data.isSample) {
const metadata = notebook.metadata as { [name: string]: unknown };
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${name} to My Notebooks`);
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
if (increaseDownloadResponse.data) {
traceSuccess(
Action.NotebooksGalleryDownload,
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
startKey
);
onComplete(increaseDownloadResponse.data);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryDownload,
{
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
}
clearInProgressMessage();
},
"Cancel",
undefined
);
} }
export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => { export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
if (!newCellId) { if (!newCellId) {
return; return;

View File

@@ -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 });

View File

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

View File

@@ -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) => ({
@@ -69,9 +72,40 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
if (tab.tabId === activeTab.tabId && tabIndex !== -1) { if (tab.tabId === activeTab.tabId && tabIndex !== -1) {
const tabToTheRight = updatedTabs[tabIndex]; const tabToTheRight = updatedTabs[tabIndex];
const lastOpenTab = updatedTabs[updatedTabs.length - 1]; const lastOpenTab = updatedTabs[updatedTabs.length - 1];
set({ activeTab: tabToTheRight || lastOpenTab }); const newActiveTab = tabToTheRight ?? lastOpenTab;
set({ activeTab: newActiveTab });
if (newActiveTab) {
newActiveTab.onActivate();
}
} }
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 });
}
}
},
})); }));

View File

@@ -15,18 +15,13 @@
"target": "es2017", "target": "es2017",
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"lib": [ "lib": ["es5", "es6", "dom"],
"es5",
"es6",
"dom"
],
"jsx": "react", "jsx": "react",
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"noEmit": true, "noEmit": true,
"types": [ "types": ["jest"],
"jest" "baseUrl": "src"
]
}, },
"typedocOptions": { "typedocOptions": {
"entryPoints": [ "entryPoints": [
@@ -42,11 +37,6 @@
"includes": "./src/SelfServe/Documentation", "includes": "./src/SelfServe/Documentation",
"disableSources": true "disableSources": true
}, },
"include": [ "include": ["src", "./src/**/*", "./utils/**/*"],
"./src/**/*", "exclude": ["./src/**/__mocks__/**/*"]
"./utils/**/*" }
],
"exclude": [
"./src/**/__mocks__/**/*"
]
}

View File

@@ -227,6 +227,7 @@ module.exports = function (_env = {}, argv = {}) {
rules, rules,
}, },
resolve: { resolve: {
modules: [path.resolve(__dirname, "src"), "node_modules"],
alias: { alias: {
process: "process/browser", process: "process/browser",
}, },