diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index bb8a2787b..f33c2650a 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -339,9 +339,11 @@ export enum ConflictOperationType { } export enum ConnectionStatusType { + Connect = "Connect", Connecting = "Connecting", Connected = "Connected", Failed = "Connection Failed", + ReConnect = "Reconnect", } export const EmulatorMasterKey = @@ -353,15 +355,32 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta export class Notebook { public static readonly defaultBasePath = "./notebooks"; - public static readonly heartbeatDelayMs = 5000; + public static readonly heartbeatDelayMs = 60000; public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartMaxDelayMs = 20000; public static readonly autoSaveIntervalMs = 120000; + public static readonly memoryGuageToGB = 1048576; public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it."; public static readonly mongoShellTemporarilyDownMsg = "We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation."; public static readonly cassandraShellTemporarilyDownMsg = "We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation."; + public static saveNotebookModalTitle = "Save Notebook in temporary workspace"; + public static saveNotebookModalContent = + "This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends."; + public static newNotebookModalTitle = "Create Notebook in temporary workspace"; + public static newNotebookUploadModalTitle = "Upload Notebook in temporary workspace"; + public static 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 { diff --git a/src/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index 362e95517..730a534f3 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -13,6 +13,7 @@ import { Link, PrimaryButton, ProgressIndicator, + Text, TextField, } from "@fluentui/react"; import React, { FC } from "react"; @@ -30,6 +31,7 @@ export interface DialogState { onOk: () => void, cancelLabel: string, onCancel: () => void, + contentHtml?: JSX.Element, choiceGroupProps?: IChoiceGroupProps, textFieldProps?: TextFieldProps, primaryButtonDisabled?: boolean @@ -58,6 +60,7 @@ export const useDialog: UseStore = create((set, get) => ({ onOk: () => void, cancelLabel: string, onCancel: () => void, + contentHtml?: JSX.Element, choiceGroupProps?: IChoiceGroupProps, textFieldProps?: TextFieldProps, primaryButtonDisabled?: boolean @@ -76,6 +79,7 @@ export const useDialog: UseStore = create((set, get) => ({ get().closeDialog(); onCancel && onCancel(); }, + contentHtml, choiceGroupProps, textFieldProps, primaryButtonDisabled, @@ -124,6 +128,7 @@ export interface DialogProps { type?: DialogType; showCloseButton?: boolean; onDismiss?: () => void; + contentHtml?: JSX.Element; } const DIALOG_MIN_WIDTH = "400px"; @@ -150,6 +155,7 @@ export const Dialog: FC = () => { type, showCloseButton, onDismiss, + contentHtml, } = props || {}; const dialogProps: IDialogProps = { @@ -191,6 +197,7 @@ export const Dialog: FC = () => { {linkProps.linkText} )} + {contentHtml && {contentHtml}} {progressIndicatorProps && } diff --git a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx index 3bbc6a538..e65c44445 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx @@ -17,6 +17,8 @@ import Explorer from "../../Explorer"; import { NotebookClientV2 } from "../../Notebook/NotebookClientV2"; import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper"; import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer"; +import { NotebookUtil } from "../../Notebook/NotebookUtil"; +import { useNotebook } from "../../Notebook/useNotebook"; import { Dialog, TextFieldProps, useDialog } from "../Dialog"; import { NotebookMetadataComponent } from "./NotebookMetadataComponent"; import "./NotebookViewerComponent.less"; @@ -146,7 +148,9 @@ export class NotebookViewerComponent { - if (!this.notebookManager) { - const NotebookManager = await ( - 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(); + this.initiateAndRefreshNotebookList(); + useNotebook.getState().setIsRefreshed(false); }, - (state) => state.isNotebookEnabled + (state) => state.isNotebookEnabled || state.isRefreshed ); this.resourceTree = new ResourceTreeAdapter(this); @@ -212,6 +202,23 @@ export default class Explorer { this.refreshExplorer(); } + public async initiateAndRefreshNotebookList(): Promise { + 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 { const addSynapseLinkDialogProps: DialogProps = { linkProps: { @@ -345,23 +352,7 @@ export default class Explorer { return; } 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 { + if (userContext.features.phoenix === false) { await this.ensureNotebookWorkspaceRunning(); const connectionInfo = await listConnectionInfo( userContext.subscriptionId, @@ -376,13 +367,59 @@ export default class Explorer { }); } - useNotebook.getState().initializeNotebooksTree(this.notebookManager); - this.refreshNotebookList(); this._isInitializingNotebooks = false; } + public async allocateContainer(): Promise { + const notebookServerInfo = useNotebook.getState().notebookServerInfo; + const isAllocating = useNotebook.getState().isAllocating; + if (isAllocating === false && notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined) { + const provisionData = { + aadToken: userContext.authorizationToken, + subscriptionId: userContext.subscriptionId, + resourceGroup: userContext.resourceGroup, + dbAccountName: userContext.databaseAccount.name, + cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint, + }; + const connectionStatus: ContainerConnectionInfo = { + status: ConnectionStatusType.Connecting, + }; + useNotebook.getState().setConnectionInfo(connectionStatus); + try { + useNotebook.getState().setIsAllocating(true); + const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData); + if ( + connectionInfo.status === HttpStatusCodes.OK && + connectionInfo.data && + connectionInfo.data.notebookServerUrl + ) { + connectionStatus.status = ConnectionStatusType.Connected; + useNotebook.getState().setConnectionInfo(connectionStatus); + useNotebook.getState().setNotebookServerInfo({ + notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl, + authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken, + }); + this.notebookManager?.notebookClient + .getMemoryUsage() + .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo)); + useNotebook.getState().setIsAllocating(false); + } else { + connectionStatus.status = ConnectionStatusType.Failed; + useNotebook.getState().resetConatinerConnection(connectionStatus); + } + } catch (error) { + connectionStatus.status = ConnectionStatusType.Failed; + useNotebook.getState().resetConatinerConnection(connectionStatus); + throw error; + } + this.refreshNotebookList(); + + this._isInitializingNotebooks = false; + } + } + public resetNotebookWorkspace(): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { handleError( @@ -654,6 +691,9 @@ export default class Explorer { if (!notebookContentItem || !notebookContentItem.path) { throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); } + if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) { + this.allocateContainer(); + } const notebookTabs = useTabs .getState() @@ -875,9 +915,51 @@ export default class Explorer { handleError(error, "Explorer/onNewNotebookClicked"); throw new Error(error); } + const isPhoenixEnabled = NotebookUtil.isPhoenixEnabled(); + 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 ( + <> +

{Notebook.newNotebookModalContent1}

+
+

+ {Notebook.newNotebookModalContent2} + + {Notebook.learnMore} + +

+ + ); + } + private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void { const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`); const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { dataExplorerArea: Constants.Areas.Notebook, @@ -924,7 +1006,26 @@ export default class Explorer { await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item); } - public openNotebookTerminal(kind: ViewModels.TerminalKind): void { + public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise { + if (NotebookUtil.isPhoenixEnabled()) { + 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 temporary workspace, this could happen because of network issue please refresh and try again." + ); + } + } else { + this.connectToNotebookTerminal(kind); + } + } + + private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void { let title: string; switch (kind) { @@ -975,7 +1076,7 @@ export default class Explorer { notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean - ) { + ): Promise { const title = "Gallery"; const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default; const galleryTab = useTabs @@ -1079,7 +1180,27 @@ export default class Explorer { } public openUploadFilePanel(parent?: NotebookContentItem): void { - parent = parent || this.resourceTree.myNotebooksContentRoot; + if (NotebookUtil.isPhoenixEnabled()) { + useDialog.getState().showOkCancelModalDialog( + Notebook.newNotebookUploadModalTitle, + undefined, + "Upload", + async () => { + await this.allocateContainer(); + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.uploadFilePanel(parent); + }, + "Cancel", + undefined, + this.getNewNoteWarningText() + ); + } else { + parent = parent || this.resourceTree.myNotebooksContentRoot; + this.uploadFilePanel(parent); + } + } + + private uploadFilePanel(parent?: NotebookContentItem): void { useSidePanel .getState() .openSidePanel( @@ -1088,6 +1209,24 @@ export default class Explorer { ); } + public getDownloadModalConent(fileName: string): JSX.Element { + if (NotebookUtil.isPhoenixEnabled()) { + return ( + <> +

{Notebook.galleryNotebookDownloadContent1}

+
+

+ {Notebook.galleryNotebookDownloadContent2} + + {Notebook.learnMore} + +

+ + ); + } + return

Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook.

; + } + public async refreshExplorer(): Promise { userContext.authType === AuthType.ResourceToken ? this.refreshDatabaseForResourceToken() diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index a6d17e96a..222fe2fa4 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -12,6 +12,7 @@ import { useTabs } from "../../../hooks/useTabs"; import { userContext } from "../../../UserContext"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; +import { NotebookUtil } from "../../Notebook/NotebookUtil"; import { useSelectedNode } from "../../useSelectedNode"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; import * as CommandBarUtil from "./CommandBarUtil"; @@ -55,15 +56,15 @@ export const CommandBar: React.FC = ({ container }: Props) => { const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); - if ( - userContext.features.notebooksTemporarilyDown === false && - userContext.features.phoenix === true && - useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2 - ) { - uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus")); + if (NotebookUtil.isPhoenixEnabled()) { + uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus")); } - if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) { + if ( + userContext.features.phoenix === false && + userContext.features.notebooksTemporarilyDown === false && + useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2 + ) { uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); } diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 3e9e35b54..e56341d80 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -596,7 +596,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp return { iconSrc: GitHubIcon, iconAlt: label, - onCommandClick: () => + onCommandClick: () => { useSidePanel .getState() .openSidePanel( @@ -606,7 +606,8 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp gitHubClientProp={container.notebookManager.gitHubClient} junoClientProp={junoClient} /> - ), + ); + }, commandButtonLabel: label, hasPopup: false, disabled: false, diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 6549ce597..e3cf556a6 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -13,6 +13,7 @@ import { StyleConstants } from "../../../Common/Constants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; +import Explorer from "../../Explorer"; import { ConnectionStatus } from "./ConnectionStatusComponent"; 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 { key, - onRender: () => , + onRender: () => , }; }; diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less index cb9b8b656..0688e0baa 100644 --- a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less +++ b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less @@ -3,77 +3,182 @@ .connectionStatusContainer { cursor: default; align-items: center; - margin: 0 9px; border: 1px; min-height: 44px; > span { padding-right: 12px; - font-size: 13px; + font-size: 12px; font-family: @DataExplorerFont; color: @DefaultFontColor; } + &:focus{ + outline: 0px; + } } -.connectionStatusFailed{ - color: #bd1919; +.commandReactBtn { + &: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; -} - -.ringringGreen { - border: 3px solid green; - border-radius: 30px; - height: 18px; - width: 18px; + display: block; + margin-right: 8px; + width: 1em; + height: 1em; + font-size: 9px!important; + padding: 0px!important; + border-radius: 0.5em; + } + + .status::before, + .status::after { position: absolute; - margin: .4285em 0em 0em 0.07477em; - animation: pulsate 3s ease-out; - animation-iteration-count: infinite; - opacity: 0.0 -} -.ringringYellow{ - border: 3px solid #ffbf00; - border-radius: 30px; - height: 18px; - width: 18px; - position: absolute; - margin: .4285em 0em 0em 0.07477em; - animation: pulsate 3s ease-out; - animation-iteration-count: infinite; - opacity: 0.0 -} -.ringringRed{ - border: 3px solid #bd1919; - border-radius: 30px; - height: 18px; - width: 18px; - position: absolute; - margin: .4285em 0em 0em 0.07477em; - animation: pulsate 3s ease-out; - animation-iteration-count: infinite; - opacity: 0.0 -} -@keyframes pulsate { - 0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;} - 15% {opacity: 0.8;} - 25% {opacity: 0.6;} - 45% {opacity: 0.4;} - 70% {opacity: 0.3;} - 100% {-webkit-transform: scale(.7, .7); opacity: 0.1;} -} -.locationGreenDot{ - font-size: 20px; - margin-right: 0.07em; - color: green; -} -.locationYellowDot{ - font-size: 20px; - margin-right: 0.07em; - color: #ffbf00; -} -.locationRedDot{ - font-size: 20px; - margin-right: 0.07em; - color: #bd1919; -} \ No newline at end of file + content: ""; + } + + .status::before { + top: 0; + left: 0; + width: 1em; + height: 1em; + background-color: rgba(#fff, 0.1); + border-radius: 100%; + opacity: 1; + transform: translate3d(0, 0, 0) scale(0); + } + + .connected{ + background-color: green; + box-shadow: + 0 0 0 0em rgba(green, 0), + 0em 0.05em 0.1em rgba(#000000, 0.2); + transform: translate3d(0, 0, 0) scale(1); + } + .connecting{ + background-color:#ffbf00; + box-shadow: + 0 0 0 0em rgba(#ffbf00, 0), + 0em 0.05em 0.1em rgba(#000000, 0.2); + transform: translate3d(0, 0, 0) scale(1); + } + .failed{ + background-color:#bd1919; + box-shadow: + 0 0 0 0em rgba(#bd1919, 0), + 0em 0.05em 0.1em rgba(#000000, 0.2); + transform: translate3d(0, 0, 0) scale(1); + } + + .status.connecting.is-animating { + animation: status-outer-connecting 3000ms infinite; + } + .status.failed.is-animating { + animation: status-outer-failed 3000ms infinite; + } + .status.connected.is-animating { + animation: status-outer-connected 3000ms infinite; + } + @keyframes status-outer-connected { + + 0% { + 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); + } + } \ No newline at end of file diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx index 34066b201..16b804a51 100644 --- a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx +++ b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx @@ -1,17 +1,21 @@ import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react"; +import { ActionButton } from "@fluentui/react/lib/Button"; import * as React from "react"; -import { ConnectionStatusType } from "../../../Common/Constants"; +import "../../../../less/hostedexplorer.less"; +import { ConnectionStatusType, Notebook } from "../../../Common/Constants"; +import Explorer from "../../Explorer"; import { useNotebook } from "../../Notebook/useNotebook"; import "../CommandBar/ConnectionStatusComponent.less"; - -export const ConnectionStatus: React.FC = (): JSX.Element => { +interface Props { + container: Explorer; +} +export const ConnectionStatus: React.FC = ({ container }: Props): JSX.Element => { const [second, setSecond] = React.useState("00"); const [minute, setMinute] = React.useState("00"); const [isActive, setIsActive] = React.useState(false); const [counter, setCounter] = React.useState(0); - const [statusColor, setStatusColor] = React.useState("locationYellowDot"); - const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow"); - const toolTipContent = "Hosted runtime status."; + const [statusColor, setStatusColor] = React.useState(""); + const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace."); React.useEffect(() => { let intervalId: NodeJS.Timeout; @@ -39,34 +43,65 @@ export const ConnectionStatus: React.FC = (): JSX.Element => { }; const connectionInfo = useNotebook((state) => state.connectionInfo); - if (!connectionInfo) { - return <>; + const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo); + + const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0; + const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0; + + if ( + connectionInfo && + (connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.ReConnect) + ) { + return ( + container.allocateContainer()}> + + + + {connectionInfo.status} + + + + ); } + if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) { setIsActive(true); + setStatusColor("status connecting is-animating"); + setToolTipContent("Connecting to temporary workspace."); } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) { stopTimer(); - setStatusColor("locationGreenDot"); - setStatusColorAnimation("ringringGreen"); + setStatusColor("status connected is-animating"); + setToolTipContent("Connected to temporary workspace."); } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) { stopTimer(); - setStatusColor("locationRedDot"); - setStatusColorAnimation("ringringRed"); + setStatusColor("status failed is-animating"); + setToolTipContent("Click here to Reconnect to temporary workspace."); } return ( - - -
-
- -
- - {connectionInfo.status} - - {connectionInfo.status === ConnectionStatusType.Connecting && isActive && ( - - )} -
-
+ ) => + connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault() + } + > + + + + + {connectionInfo.status} + + {connectionInfo.status === ConnectionStatusType.Connecting && isActive && ( + + )} + {connectionInfo.status === ConnectionStatusType.Connected && !isActive && ( + 0.8 ? "lowMemory" : ""} + description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"} + percentComplete={usedGB / totalGB} + /> + )} + + + ); }; diff --git a/src/Explorer/Notebook/NotebookContainerClient.ts b/src/Explorer/Notebook/NotebookContainerClient.ts index 91cedca06..c0e6a04e8 100644 --- a/src/Explorer/Notebook/NotebookContainerClient.ts +++ b/src/Explorer/Notebook/NotebookContainerClient.ts @@ -2,12 +2,15 @@ * Notebook container related stuff */ import * as Constants from "../../Common/Constants"; +import { ConnectionStatusType } from "../../Common/Constants"; import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import * as Logger from "../../Common/Logger"; import * as DataModels from "../../Contracts/DataModels"; +import { ContainerConnectionInfo } from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import { NotebookUtil } from "./NotebookUtil"; import { useNotebook } from "./useNotebook"; export class NotebookContainerClient { @@ -42,7 +45,7 @@ export class NotebookContainerClient { }, delayMs); } - private async getMemoryUsage(): Promise { + public async getMemoryUsage(): Promise { const notebookServerInfo = useNotebook.getState().notebookServerInfo; if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) { const error = "No server endpoint detected"; @@ -75,6 +78,12 @@ export class NotebookContainerClient { freeKB: memoryUsageInfo.free, }; } + } else if (NotebookUtil.isPhoenixEnabled()) { + const connectionStatus: ContainerConnectionInfo = { + status: ConnectionStatusType.ReConnect, + }; + useNotebook.getState().resetConatinerConnection(connectionStatus); + useNotebook.getState().setIsRefreshed(true); } return undefined; } catch (error) { @@ -84,6 +93,13 @@ export class NotebookContainerClient { "Connection lost with Notebook server. Attempting to reconnect..." ); } + if (NotebookUtil.isPhoenixEnabled()) { + const connectionStatus: ContainerConnectionInfo = { + status: ConnectionStatusType.Failed, + }; + useNotebook.getState().resetConatinerConnection(connectionStatus); + useNotebook.getState().setIsRefreshed(true); + } this.onConnectionLost(); return undefined; } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index ed66caab1..21cb86085 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -212,6 +212,7 @@ export default class NotebookManager { "Cancel", () => reject(new Error("Commit dialog canceled")), undefined, + undefined, { label: "Commit message", autoAdjustHeight: true, diff --git a/src/Explorer/Notebook/NotebookUtil.ts b/src/Explorer/Notebook/NotebookUtil.ts index b060533d7..68d562f38 100644 --- a/src/Explorer/Notebook/NotebookUtil.ts +++ b/src/Explorer/Notebook/NotebookUtil.ts @@ -3,6 +3,7 @@ import { AppState, selectors } from "@nteract/core"; import domtoimage from "dom-to-image"; import Html2Canvas from "html2canvas"; import path from "path"; +import { userContext } from "../../UserContext"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as StringUtils from "../../Utils/StringUtils"; import { SnapshotFragment } from "./NotebookComponent/types"; @@ -328,4 +329,16 @@ export class NotebookUtil { link.click(); document.body.removeChild(link); } + + public static getNotebookBtnTitle(fileName: string): string { + if (this.isPhoenixEnabled()) { + return `Download to ${fileName}`; + } else { + return `Download to my notebooks`; + } + } + + public static isPhoenixEnabled(): boolean { + return userContext.features.notebooksTemporarilyDown === false && userContext.features.phoenix === true; + } } diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index 2f4b086b1..3426b56e7 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -2,10 +2,12 @@ import { cloneDeep } from "lodash"; import create, { UseStore } from "zustand"; import { AuthType } from "../../AuthType"; import * as Constants from "../../Common/Constants"; +import { ConnectionStatusType } from "../../Common/Constants"; import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; import * as Logger from "../../Common/Logger"; import { configContext } from "../../ConfigContext"; import * as DataModels from "../../Contracts/DataModels"; +import { ContainerConnectionInfo } from "../../Contracts/DataModels"; import { IPinnedRepo } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; @@ -14,6 +16,7 @@ import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import NotebookManager from "./NotebookManager"; +import { NotebookUtil } from "./NotebookUtil"; interface NotebookState { isNotebookEnabled: boolean; @@ -28,8 +31,10 @@ interface NotebookState { myNotebooksContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem; - connectionInfo: DataModels.ContainerConnectionInfo; + connectionInfo: ContainerConnectionInfo; notebookFolderName: string; + isAllocating: boolean; + isRefreshed: boolean; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; @@ -46,7 +51,10 @@ interface NotebookState { deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; - setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void; + setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void; + setIsAllocating: (isAllocating: boolean) => void; + resetConatinerConnection: (connectionStatus: ContainerConnectionInfo) => void; + setIsRefreshed: (isAllocating: boolean) => void; } export const useNotebook: UseStore = create((set, get) => ({ @@ -69,8 +77,12 @@ export const useNotebook: UseStore = create((set, get) => ({ myNotebooksContentRoot: undefined, gitHubNotebooksContentRoot: undefined, galleryContentRoot: undefined, - connectionInfo: undefined, + connectionInfo: { + status: ConnectionStatusType.Connect, + }, notebookFolderName: undefined, + isAllocating: false, + isRefreshed: false, setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => @@ -175,7 +187,7 @@ export const useNotebook: UseStore = create((set, get) => ({ isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { - const notebookFolderName = userContext.features.phoenix === true ? "Temporary Notebooks" : "My Notebooks"; + const notebookFolderName = NotebookUtil.isPhoenixEnabled() === true ? "Temporary Notebooks" : "My Notebooks"; set({ notebookFolderName }); const myNotebooksContentRoot = { name: get().notebookFolderName, @@ -256,5 +268,15 @@ export const useNotebook: UseStore = create((set, get) => ({ set({ gitHubNotebooksContentRoot }); } }, - setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }), + setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }), + setIsAllocating: (isAllocating: boolean) => set({ isAllocating }), + resetConatinerConnection: (connectionStatus: ContainerConnectionInfo): void => { + useNotebook.getState().setConnectionInfo(connectionStatus); + useNotebook.getState().setNotebookServerInfo({ + notebookServerEndpoint: undefined, + authToken: undefined, + }); + useNotebook.getState().setIsAllocating(false); + }, + setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }), })); diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx index d55a1feaa..960f25845 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -77,7 +77,7 @@ export const CopyNotebookPane: FunctionComponent = ({ selectedLocation.repo )} - ${selectedLocation.branch}`; } else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) { - destination = "My Notebooks Scratch"; + destination = useNotebook.getState().notebookFolderName; } clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 8d6bece87..f31a4a52b 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -128,7 +128,12 @@ export const ResourceTree: React.FC = ({ container }: Resourc notebooksTree.children.push(buildGalleryNotebooksTree()); } - if (myNotebooksContentRoot && useNotebook.getState().connectionInfo.status == ConnectionStatusType.Connected) { + if ( + myNotebooksContentRoot && + ((NotebookUtil.isPhoenixEnabled() && + useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected) || + userContext.features.phoenix === false) + ) { notebooksTree.children.push(buildMyNotebooksTree()); } if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { @@ -162,7 +167,11 @@ export const ResourceTree: React.FC = ({ container }: Resourc myNotebooksContentRoot, (item: NotebookContentItem) => { container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { + if ( + hasOpened && + userContext.features.notebooksTemporarilyDown === false && + userContext.features.phoenix === false + ) { mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); } }); @@ -181,7 +190,11 @@ export const ResourceTree: React.FC = ({ container }: Resourc gitHubNotebooksContentRoot, (item: NotebookContentItem) => { container.openNotebook(item).then((hasOpened) => { - if (hasOpened) { + if ( + hasOpened && + userContext.features.notebooksTemporarilyDown === false && + userContext.features.phoenix === false + ) { mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item); } }); @@ -213,23 +226,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc }, }, ]; - const connectGitContextMenu: TreeNodeMenuItem[] = [ - { - label: "Connect to GitHub", - onClick: () => - useSidePanel - .getState() - .openSidePanel( - "Connect to GitHub", - - ), - }, - ]; - gitHubNotebooksTree.contextMenu = isConnected ? manageGitContextMenu : connectGitContextMenu; + gitHubNotebooksTree.contextMenu = manageGitContextMenu; gitHubNotebooksTree.isExpanded = true; gitHubNotebooksTree.isAlphaSorted = true; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 9c5da6b19..a5e75ded3 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -45,7 +45,6 @@ import UserDefinedFunction from "./UserDefinedFunction"; export class ResourceTreeAdapter implements ReactAdapter { public static readonly MyNotebooksTitle = "My Notebooks"; - public static readonly MyNotebooksScratchTitle = "My Notebooks Scratch"; public static readonly GitHubReposTitle = "GitHub repos"; private static readonly DataTitle = "DATA"; diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts index aa4ed9322..bb7797afd 100644 --- a/src/Phoenix/PhoenixClient.ts +++ b/src/Phoenix/PhoenixClient.ts @@ -1,7 +1,5 @@ -import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants"; +import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { configContext } from "../ConfigContext"; -import { ContainerConnectionInfo } from "../Contracts/DataModels"; -import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { userContext } from "../UserContext"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; @@ -15,7 +13,6 @@ export interface IPhoenixConnectionInfoResult { } export interface IProvosionData { cosmosEndpoint: string; - resourceId: string; dbAccountName: string; aadToken: string; resourceGroup: string; @@ -26,11 +23,7 @@ export class PhoenixClient { provisionData: IProvosionData ): Promise> { try { - const connectionStatus: ContainerConnectionInfo = { - status: ConnectionStatusType.Connecting, - }; - useNotebook.getState().setConnectionInfo(connectionStatus); - const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, { + const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/allocate`, { method: "POST", headers: PhoenixClient.getHeaders(), body: JSON.stringify(provisionData), @@ -38,31 +31,20 @@ export class PhoenixClient { let data: IPhoenixConnectionInfoResult; if (response.status === HttpStatusCodes.OK) { 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 { status: response.status, data, }; } catch (error) { - const connectionStatus: ContainerConnectionInfo = { - status: ConnectionStatusType.Failed, - }; - useNotebook.getState().setConnectionInfo(connectionStatus); console.error(error); throw error; } } 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) { const error = `${phoenixEndpoint} not allowed as juno endpoint`; console.error(error); @@ -73,7 +55,7 @@ export class PhoenixClient { } public getPhoenixContainerPoolingEndPoint(): string { - return `${PhoenixClient.getPhoenixEndpoint()}/api/containerpooling`; + return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer`; } private static getHeaders(): HeadersInit { const authorizationHeader = getAuthorizationHeader(); diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 8342d9637..f2463583a 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -20,6 +20,7 @@ export type Features = { readonly enableKoResourceTree: boolean; readonly hostedDataExplorer: boolean; readonly junoEndpoint?: string; + readonly phoenixEndpoint?: string; readonly livyEndpoint?: string; readonly notebookBasePath?: string; readonly notebookServerToken?: string; @@ -68,6 +69,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear mongoProxyEndpoint: get("mongoproxyendpoint"), mongoProxyAPIs: get("mongoproxyapis"), junoEndpoint: get("junoendpoint"), + phoenixEndpoint: get("phoenixendpoint"), livyEndpoint: get("livyendpoint"), notebookBasePath: get("notebookbasepath"), notebookServerToken: get("notebookservertoken"), diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index 6ed56f05c..fb5b7dcb8 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -10,6 +10,7 @@ import { SortBy, } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import Explorer from "../Explorer/Explorer"; +import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; @@ -225,67 +226,89 @@ export function downloadItem( const name = data.name; useDialog.getState().showOkCancelModalDialog( `Download to ${useNotebook.getState().notebookFolderName}`, - `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, + undefined, "Download", async () => { - const clearInProgressMessage = logConsoleProgress( - `Downloading ${name} to ${useNotebook.getState().notebookFolderName}` + if (NotebookUtil.isPhoenixEnabled()) { + 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, downloadCount: data.downloads, isSample: data.isSample, - }); + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); - try { - const response = await junoClient.getNotebookContent(data.id); - if (!response.data) { - throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`); - } + handleError(error, "GalleryUtils/downloadItem", `Failed to download ${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 ${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 - ); + clearInProgressMessage(); } - export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => { if (!newCellId) { return;