From 95c9b7ee3156a571a428a16943b0d3c37438fda2 Mon Sep 17 00:00:00 2001 From: kcheekuri <88904658+kcheekuri@users.noreply.github.com> Date: Sat, 4 Sep 2021 02:04:26 -0400 Subject: [PATCH] Users/kcheekuri/aciconatinerpooling (#1008) * initial changes for CP * Added container unprovisioning * Added postgreSQL terminal * changed postgres terminal -> shell * Initialize Container Request payload change * added postgres button * Added notebookServerInfo * Feature flag enabling and integration with phoenix * Remove postgre implementations * fix issues * fix format issues * fix format issues-1 * fix format issues-2 * fix format issues-3 * fix format issues-4 * fix format issues-5 * connection status component * connection status component-1 * connection status component-2 * connection status component-3 * address issues * removal of ms * removal of ms * removal of ms-1 * removal of time after connected * removal of time after connected * removing unnecessary code Co-authored-by: Srinath Narayanan Co-authored-by: Bala Lakshmi Narayanasami --- src/Common/Constants.ts | 8 ++ src/Contracts/DataModels.ts | 7 ++ src/Explorer/Controls/Dialog.tsx | 3 +- .../SettingsComponent.test.tsx.snap | 2 + src/Explorer/Explorer.tsx | 72 ++++++++++++----- .../CommandBar/CommandBarComponentAdapter.tsx | 2 + .../CommandBarComponentButtonFactory.tsx | 5 +- .../Menus/CommandBar/CommandBarUtil.tsx | 8 ++ .../CommandBar/ConnectionStatusComponent.less | 79 +++++++++++++++++++ .../CommandBar/ConnectionStatusComponent.tsx | 77 ++++++++++++++++++ .../Notebook/NotebookComponent/epics.ts | 2 +- .../Notebook/NotebookContainerClient.ts | 2 +- .../Notebook/NotebookContentClient.ts | 6 +- src/Explorer/Notebook/useNotebook.ts | 20 +++-- .../CopyNotebookPane/CopyNotebookPane.tsx | 3 + .../CopyNotebookPaneComponent.tsx | 6 +- .../GitHubReposPanel.test.tsx.snap | 1 + .../StringInputPane.test.tsx.snap | 1 + src/Explorer/Tree/ResourceTree.tsx | 29 +++++-- src/Explorer/Tree/ResourceTreeAdapter.tsx | 19 ++--- src/Main.tsx | 1 + src/Phoenix/PhoenixClient.ts | 70 ++++++++++++++++ src/Platform/Hosted/extractFeatures.ts | 2 + src/Utils/GalleryUtils.ts | 7 +- src/hooks/useKnockoutExplorer.ts | 3 + 25 files changed, 375 insertions(+), 60 deletions(-) create mode 100644 src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less create mode 100644 src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx create mode 100644 src/Phoenix/PhoenixClient.ts diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 293e1b3e8..6e13f07b5 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -96,6 +96,7 @@ export class Flights { public static readonly AutoscaleTest = "autoscaletest"; public static readonly PartitionKeyTest = "partitionkeytest"; public static readonly PKPartitionKeyTest = "pkpartitionkeytest"; + public static readonly Phoenix = "phoenix"; } export class AfecFeatures { @@ -337,6 +338,13 @@ export enum ConflictOperationType { Delete = "delete", } +export enum ConnectionStatusType { + Connecting = "Connecting", + Allocating = "Allocating", + Connected = "Connected", + Failed = "Connection Failed", +} + export const EmulatorMasterKey = //[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index efd5ffb78..a8957c662 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -1,3 +1,5 @@ +import { ConnectionStatusType } from "../Common/Constants"; + export interface DatabaseAccount { id: string; name: string; @@ -496,3 +498,8 @@ export interface MemoryUsageInfo { freeKB: number; totalKB: number; } + +export interface ContainerConnectionInfo { + status: ConnectionStatusType; + //need to add ram and rom info +} diff --git a/src/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index 449627d13..362e95517 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -181,8 +181,7 @@ export const Dialog: FC = () => { text: secondaryButtonText, onClick: onSecondaryButtonClick, } - : {}; - + : undefined; return visible ? ( {choiceGroupProps && } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index b1164fe05..6f01661ea 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -34,6 +34,7 @@ exports[`SettingsComponent renders 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], + "phoenixClient": PhoenixClient {}, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], @@ -101,6 +102,7 @@ exports[`SettingsComponent renders 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], + "phoenixClient": PhoenixClient {}, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 40321286f..2cd3e79da 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -4,6 +4,7 @@ import _ from "underscore"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import * as Constants from "../Common/Constants"; +import { ConnectionStatusType } from "../Common/Constants"; import { readCollection } from "../Common/dataAccess/readCollection"; import { readDatabases } from "../Common/dataAccess/readDatabases"; import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility"; @@ -16,6 +17,7 @@ import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { useSidePanel } from "../hooks/useSidePanel"; import { useTabs } from "../hooks/useTabs"; import { IGalleryItem } from "../Juno/JunoClient"; +import { PhoenixClient } from "../Phoenix/PhoenixClient"; import * as ExplorerSettings from "../Shared/ExplorerSettings"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; @@ -87,12 +89,13 @@ export default class Explorer { }; private static readonly MaxNbDatabasesToAutoExpand = 5; - + private phoenixClient: PhoenixClient; constructor() { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { dataExplorerArea: Constants.Areas.ResourceTree, }); this._isInitializingNotebooks = false; + this.phoenixClient = new PhoenixClient(); useNotebook.subscribe( () => this.refreshCommandBarButtons(), (state) => state.isNotebooksEnabledForAccount @@ -343,19 +346,43 @@ export default class Explorer { return; } this._isInitializingNotebooks = true; + if (userContext.features.phoenix) { + const connectionStatus: DataModels.ContainerConnectionInfo = { + status: ConnectionStatusType.Allocating, + }; + useNotebook.getState().setConnectionInfo(connectionStatus); + 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) { + connectionStatus.status = ConnectionStatusType.Connected; + useNotebook.getState().setConnectionInfo(connectionStatus); - await this.ensureNotebookWorkspaceRunning(); - const connectionInfo = await listConnectionInfo( - userContext.subscriptionId, - userContext.resourceGroup, - databaseAccount.name, - "default" - ); + 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().setNotebookServerInfo({ + notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint, + authToken: userContext.features.notebookServerToken || connectionInfo.authToken, + }); + } useNotebook.getState().initializeNotebooksTree(this.notebookManager); @@ -364,7 +391,7 @@ export default class Explorer { this._isInitializingNotebooks = false; } - public resetNotebookWorkspace() { + public resetNotebookWorkspace(): void { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) { handleError( "Attempt to reset notebook workspace, but notebook is not enabled", @@ -389,7 +416,6 @@ export default class Explorer { if (!databaseAccount) { return false; } - try { const { value: workspaces } = await listByDatabaseAccount( userContext.subscriptionId, @@ -906,7 +932,7 @@ export default class Explorer { await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item); } - public openNotebookTerminal(kind: ViewModels.TerminalKind) { + public openNotebookTerminal(kind: ViewModels.TerminalKind): void { let title: string; switch (kind) { @@ -1026,7 +1052,10 @@ export default class Explorer { } public async handleOpenFileAction(path: string): Promise { - if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { + if ( + userContext.features.phoenix === false && + !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) + ) { this._openSetupNotebooksPaneForQuickstart(); } @@ -1072,10 +1101,13 @@ export default class Explorer { ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); - const isNotebookEnabled: boolean = - userContext.authType !== AuthType.ResourceToken && - ((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || - userContext.features.enableNotebooks); + let isNotebookEnabled = true; + if (!userContext.features.phoenix) { + isNotebookEnabled = + userContext.authType !== AuthType.ResourceToken && + ((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || + userContext.features.enableNotebooks); + } useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled); useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed()); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 9755075a8..452d7e881 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -54,6 +54,8 @@ export const CommandBar: React.FC = ({ container }: Props) => { const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); + uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus")); + if (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 623c8250d..3e9e35b54 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -80,8 +80,9 @@ export function createStaticCommandBarButtons( } notebookButtons.push(createOpenTerminalButton(container)); - - notebookButtons.push(createNotebookWorkspaceResetButton(container)); + if (userContext.features.phoenix === false) { + notebookButtons.push(createNotebookWorkspaceResetButton(container)); + } if ( (userContext.apiType === "Mongo" && useNotebook.getState().isShellEnabled && diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 748657816..6549ce597 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 { ConnectionStatus } from "./ConnectionStatusComponent"; import { MemoryTracker } from "./MemoryTrackerComponent"; /** @@ -201,3 +202,10 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => { onRender: () => , }; }; + +export const createConnectionStatus = (key: string): ICommandBarItemProps => { + return { + key, + onRender: () => , + }; +}; diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less new file mode 100644 index 000000000..cb9b8b656 --- /dev/null +++ b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less @@ -0,0 +1,79 @@ +@import "../../../../less/Common/Constants"; + +.connectionStatusContainer { + cursor: default; + align-items: center; + margin: 0 9px; + border: 1px; + min-height: 44px; + + > span { + padding-right: 12px; + font-size: 13px; + font-family: @DataExplorerFont; + color: @DefaultFontColor; + } +} +.connectionStatusFailed{ + color: #bd1919; +} +.ring-container { + position: relative; +} + +.ringringGreen { + border: 3px solid green; + 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 +} +.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 diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx new file mode 100644 index 000000000..7e4cfa04d --- /dev/null +++ b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx @@ -0,0 +1,77 @@ +import { Icon, ProgressIndicator, Spinner, SpinnerSize, Stack, TooltipHost } from "@fluentui/react"; +import * as React from "react"; +import { ConnectionStatusType } from "../../../Common/Constants"; +import { useNotebook } from "../../Notebook/useNotebook"; +import "../CommandBar/ConnectionStatusComponent.less"; + +export const ConnectionStatus: React.FC = (): 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."; + React.useEffect(() => { + let intervalId: NodeJS.Timeout; + + if (isActive) { + intervalId = setInterval(() => { + const secondCounter = counter % 60; + const minuteCounter = Math.floor(counter / 60); + const computedSecond: string = String(secondCounter).length === 1 ? `0${secondCounter}` : `${secondCounter}`; + const computedMinute: string = String(minuteCounter).length === 1 ? `0${minuteCounter}` : `${minuteCounter}`; + + setSecond(computedSecond); + setMinute(computedMinute); + + setCounter((counter) => counter + 1); + }, 1000); + } + return () => clearInterval(intervalId); + }, [isActive, counter]); + + const stopTimer = () => { + setIsActive(false); + setCounter(0); + setSecond("00"); + setMinute("00"); + }; + + const connectionInfo = useNotebook((state) => state.connectionInfo); + if (!connectionInfo) { + return ( + + Connecting + + + ); + } + if (connectionInfo && connectionInfo.status === ConnectionStatusType.Allocating && isActive === false) { + setIsActive(true); + } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) { + stopTimer(); + setStatusColor("locationGreenDot"); + setStatusColorAnimation("ringringGreen"); + } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) { + stopTimer(); + setStatusColor("locationRedDot"); + setStatusColorAnimation("ringringRed"); + } + return ( + + +
+
+ +
+ + {connectionInfo.status} + + {connectionInfo.status === ConnectionStatusType.Allocating && isActive && ( + + )} +
+
+ ); +}; diff --git a/src/Explorer/Notebook/NotebookComponent/epics.ts b/src/Explorer/Notebook/NotebookComponent/epics.ts index 9e6616a06..da2bc35a8 100644 --- a/src/Explorer/Notebook/NotebookComponent/epics.ts +++ b/src/Explorer/Notebook/NotebookComponent/epics.ts @@ -109,7 +109,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string, const q = params.toString(); const suffix = q !== "" ? `?${q}` : ""; - const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`; + const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`; return url.replace(/^http(s)?/, "ws$1"); }; diff --git a/src/Explorer/Notebook/NotebookContainerClient.ts b/src/Explorer/Notebook/NotebookContainerClient.ts index 0e50106a7..91cedca06 100644 --- a/src/Explorer/Notebook/NotebookContainerClient.ts +++ b/src/Explorer/Notebook/NotebookContainerClient.ts @@ -56,7 +56,7 @@ export class NotebookContainerClient { const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig(); try { - const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, { + const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, { method: "GET", headers: { Authorization: authToken, diff --git a/src/Explorer/Notebook/NotebookContentClient.ts b/src/Explorer/Notebook/NotebookContentClient.ts index 5ca408c2a..a0e368020 100644 --- a/src/Explorer/Notebook/NotebookContentClient.ts +++ b/src/Explorer/Notebook/NotebookContentClient.ts @@ -36,7 +36,10 @@ export class NotebookContentClient { * * @param parent parent folder */ - public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise { + public async createNewNotebookFile( + parent: NotebookContentItem, + isGithubTree?: boolean + ): Promise { if (!parent || parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent must be a directory: ${parent}`); } @@ -99,7 +102,6 @@ export class NotebookContentClient { if (!parent || parent.type !== NotebookContentItemType.Directory) { throw new Error(`Parent must be a directory: ${parent}`); } - const filepath = NotebookUtil.getFilePath(parent.path, name); if (await this.checkIfFilepathExists(filepath)) { throw new Error(`File already exists: ${filepath}`); diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index 5a2c97b3e..348f7278e 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -28,6 +28,8 @@ interface NotebookState { myNotebooksContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem; + connectionInfo: DataModels.ContainerConnectionInfo; + NotebookFolderName: string; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; @@ -43,6 +45,7 @@ interface NotebookState { deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; + setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void; } export const useNotebook: UseStore = create((set, get) => ({ @@ -65,6 +68,8 @@ export const useNotebook: UseStore = create((set, get) => ({ myNotebooksContentRoot: undefined, gitHubNotebooksContentRoot: undefined, galleryContentRoot: undefined, + connectionInfo: undefined, + NotebookFolderName: userContext.features.phoenix ? "My Notebooks Scratch" : "My Notebooks", setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => @@ -169,7 +174,7 @@ export const useNotebook: UseStore = create((set, get) => ({ }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { const myNotebooksContentRoot = { - name: "My Notebooks", + name: get().NotebookFolderName, path: get().notebookBasePath, type: NotebookContentItemType.Directory, }; @@ -178,13 +183,11 @@ export const useNotebook: UseStore = create((set, get) => ({ path: "Gallery", type: NotebookContentItemType.File, }; - const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn() - ? { - name: "GitHub repos", - path: "PsuedoDir", - type: NotebookContentItemType.Directory, - } - : undefined; + const gitHubNotebooksContentRoot = { + name: "GitHub repos", + path: "PsuedoDir", + type: NotebookContentItemType.Directory, + }; set({ myNotebooksContentRoot, galleryContentRoot, @@ -246,4 +249,5 @@ export const useNotebook: UseStore = create((set, get) => ({ set({ gitHubNotebooksContentRoot }); } }, + setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }), })); diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx index 3c63c00ae..d55a1feaa 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx @@ -5,6 +5,7 @@ import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { useSidePanel } from "../../../hooks/useSidePanel"; import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; +import { userContext } from "../../../UserContext"; import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; @@ -75,6 +76,8 @@ export const CopyNotebookPane: FunctionComponent = ({ selectedLocation.owner, selectedLocation.repo )} - ${selectedLocation.branch}`; + } else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) { + destination = "My Notebooks Scratch"; } clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`); diff --git a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx index 47bac0e2e..66ef0d584 100644 --- a/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx +++ b/src/Explorer/Panes/CopyNotebookPane/CopyNotebookPaneComponent.tsx @@ -12,6 +12,7 @@ import { import React, { FormEvent, FunctionComponent } from "react"; import { IPinnedRepo } from "../../../Juno/JunoClient"; import * as GitHubUtils from "../../../Utils/GitHubUtils"; +import { useNotebook } from "../../Notebook/useNotebook"; import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter"; interface Location { @@ -46,11 +47,10 @@ export const CopyNotebookPaneComponent: FunctionComponent const getDropDownOptions = (): IDropdownOption[] => { const options: IDropdownOption[] = []; - options.push({ key: "MyNotebooks-Item", - text: ResourceTreeAdapter.MyNotebooksTitle, - title: ResourceTreeAdapter.MyNotebooksTitle, + text: useNotebook.getState().NotebookFolderName, + title: useNotebook.getState().NotebookFolderName, data: { type: "MyNotebooks", } as Location, diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 7406a14ee..e7ed75e05 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -23,6 +23,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], + "phoenixClient": PhoenixClient {}, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 80bb050de..0c1815afe 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -13,6 +13,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "isTabsContentExpanded": [Function], "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], + "phoenixClient": PhoenixClient {}, "provideFeedbackEmail": [Function], "queriesClient": QueriesClient { "container": [Circular], diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 172c9caf9..9f5396ed2 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -131,14 +131,14 @@ export const ResourceTree: React.FC = ({ container }: Resourc if (myNotebooksContentRoot) { notebooksTree.children.push(buildMyNotebooksTree()); } - if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) { // collapse all other notebook nodes notebooksTree.children.forEach((node) => (node.isExpanded = false)); - notebooksTree.children.push(buildGitHubNotebooksTree()); + notebooksTree.children.push(buildGitHubNotebooksTree(true)); + } else if (container.notebookManager && !container.notebookManager.gitHubOAuthService.isLoggedIn()) { + notebooksTree.children.push(buildGitHubNotebooksTree(false)); } } - return notebooksTree; }; @@ -178,7 +178,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc return myNotebooksTree; }; - const buildGitHubNotebooksTree = (): TreeNode => { + const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => { const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode( gitHubNotebooksContentRoot, (item: NotebookContentItem) => { @@ -190,8 +190,7 @@ export const ResourceTree: React.FC = ({ container }: Resourc }, true ); - - gitHubNotebooksTree.contextMenu = [ + const manageGitContextMenu: TreeNodeMenuItem[] = [ { label: "Manage GitHub settings", onClick: () => @@ -216,7 +215,23 @@ 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.isExpanded = true; gitHubNotebooksTree.isAlphaSorted = true; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 6a9352db2..7941446d3 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -45,6 +45,7 @@ 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"; @@ -130,9 +131,8 @@ export class ResourceTreeAdapter implements ReactAdapter { path: "Gallery", type: NotebookContentItemType.File, }; - this.myNotebooksContentRoot = { - name: ResourceTreeAdapter.MyNotebooksTitle, + name: useNotebook.getState().NotebookFolderName, path: useNotebook.getState().notebookBasePath, type: NotebookContentItemType.Directory, }; @@ -146,16 +146,11 @@ export class ResourceTreeAdapter implements ReactAdapter { }) ); } - - if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - this.gitHubNotebooksContentRoot = { - name: ResourceTreeAdapter.GitHubReposTitle, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - }; - } else { - this.gitHubNotebooksContentRoot = undefined; - } + this.gitHubNotebooksContentRoot = { + name: ResourceTreeAdapter.GitHubReposTitle, + path: ResourceTreeAdapter.PseudoDirPath, + type: NotebookContentItemType.Directory, + }; return Promise.all(refreshTasks); } diff --git a/src/Main.tsx b/src/Main.tsx index 80d008b17..c38012b64 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -37,6 +37,7 @@ import "./Explorer/Controls/TreeComponent/treeComponent.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less"; import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts new file mode 100644 index 000000000..155672cd3 --- /dev/null +++ b/src/Phoenix/PhoenixClient.ts @@ -0,0 +1,70 @@ +import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants"; +import { configContext } from "../ConfigContext"; +import * as DataModels from "../Contracts/DataModels"; +import { useNotebook } from "../Explorer/Notebook/useNotebook"; +import { userContext } from "../UserContext"; +import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; + +export interface IPhoenixResponse { + 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 { + public async containerConnectionInfo( + provisionData: IProvosionData + ): Promise> { + const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, { + method: "POST", + headers: PhoenixClient.getHeaders(), + body: JSON.stringify(provisionData), + }); + let data: IPhoenixConnectionInfoResult; + if (response.status === HttpStatusCodes.OK) { + data = await response.json(); + } else { + const connectionStatus: DataModels.ContainerConnectionInfo = { + status: ConnectionStatusType.Failed, + }; + useNotebook.getState().setConnectionInfo(connectionStatus); + } + + return { + status: response.status, + data, + }; + } + + public static getPhoenixEndpoint(): string { + const 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); + throw new Error(error); + } + + return phoenixEndpoint; + } + + public getPhoenixContainerPoolingEndPoint(): string { + return `${PhoenixClient.getPhoenixEndpoint()}/api/containerpooling`; + } + private static getHeaders(): HeadersInit { + const authorizationHeader = getAuthorizationHeader(); + return { + [authorizationHeader.header]: authorizationHeader.token, + [HttpHeaders.contentType]: "application/json", + }; + } +} diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 6e4992fc6..29c106a6e 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -11,6 +11,7 @@ export type Features = { autoscaleDefault: boolean; partitionKeyDefault: boolean; partitionKeyDefault2: boolean; + phoenix: boolean; readonly enableSDKoperations: boolean; readonly enableSpark: boolean; readonly enableTtl: boolean; @@ -76,5 +77,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear partitionKeyDefault: "true" === get("partitionkeytest"), partitionKeyDefault2: "true" === get("pkpartitionkeytest"), notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"), + phoenix: "true" === get("phoenix"), }; } diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index d39a74a04..db3f081de 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 { useNotebook } from "../Explorer/Notebook/useNotebook"; import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor"; @@ -223,11 +224,13 @@ export function downloadItem( const name = data.name; useDialog.getState().showOkCancelModalDialog( - "Download to My Notebooks", + `Download to ${useNotebook.getState().NotebookFolderName}`, `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, "Download", async () => { - const clearInProgressMessage = logConsoleProgress(`Downloading ${name} to My Notebooks`); + const clearInProgressMessage = logConsoleProgress( + `Downloading ${name} to ${useNotebook.getState().NotebookFolderName}` + ); const startKey = traceStart(Action.NotebooksGalleryDownload, { notebookId: data.id, downloadCount: data.downloads, diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 24c100eb8..6e45e26d8 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -339,6 +339,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) { userContext.features.partitionKeyDefault2 = true; } + if (inputs.flights.indexOf(Flights.Phoenix) !== -1) { + userContext.features.phoenix = true; + } } }