import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { cloneDeep } from "lodash"; import { PhoenixClient } from "Phoenix/PhoenixClient"; 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, ContainerInfo } from "../../Contracts/DataModels"; import { useTabs } from "../../hooks/useTabs"; import { IPinnedRepo } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import NotebookManager from "./NotebookManager"; interface NotebookState { isNotebookEnabled: boolean; isNotebooksEnabledForAccount: boolean; notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo; isSynapseLinkUpdating: boolean; memoryUsageInfo: DataModels.MemoryUsageInfo; isShellEnabled: boolean; notebookBasePath: string; isInitializingNotebooks: boolean; myNotebooksContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem; connectionInfo: ContainerConnectionInfo; notebookFolderName: string; isAllocating: boolean; isRefreshed: boolean; containerStatus: ContainerInfo; isPhoenixNotebooks: boolean; isPhoenixFeatures: boolean; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) => void; setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void; setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void; setIsShellEnabled: (isShellEnabled: boolean) => void; setNotebookBasePath: (notebookBasePath: string) => void; setNotebookFolderName: (notebookFolderName: string) => void; refreshNotebooksEnabledStateForAccount: () => Promise; findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void; updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; initializeNotebooksTree: (notebookManager: NotebookManager) => Promise; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void; setIsAllocating: (isAllocating: boolean) => void; resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void; setIsRefreshed: (isAllocating: boolean) => void; setContainerStatus: (containerStatus: ContainerInfo) => void; getPhoenixStatus: () => Promise; setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void; setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void; } export const useNotebook: UseStore = create((set, get) => ({ isNotebookEnabled: false, isNotebooksEnabledForAccount: false, notebookServerInfo: { notebookServerEndpoint: undefined, authToken: undefined, forwardingId: undefined, }, sparkClusterConnectionInfo: { userName: undefined, password: undefined, endpoints: [], }, isSynapseLinkUpdating: false, memoryUsageInfo: undefined, isShellEnabled: false, notebookBasePath: Constants.Notebook.defaultBasePath, isInitializingNotebooks: false, myNotebooksContentRoot: undefined, gitHubNotebooksContentRoot: undefined, galleryContentRoot: undefined, connectionInfo: { status: ConnectionStatusType.Connect, }, notebookFolderName: undefined, isAllocating: false, isRefreshed: false, containerStatus: { status: undefined, durationLeftInMinutes: undefined, notebookServerInfo: undefined, }, isPhoenixNotebooks: undefined, isPhoenixFeatures: undefined, setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => set({ notebookServerInfo }), setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) => set({ sparkClusterConnectionInfo }), setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }), setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }), setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }), setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }), setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }), refreshNotebooksEnabledStateForAccount: async (): Promise => { await get().getPhoenixStatus(); const { databaseAccount, authType } = userContext; if ( authType === AuthType.EncryptedToken || authType === AuthType.ResourceToken || authType === AuthType.MasterKey ) { set({ isNotebooksEnabledForAccount: false }); return; } const firstWriteLocation = databaseAccount?.properties?.writeLocations && databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase(); const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`; const authorizationHeader = getAuthorizationHeader(); try { const response = await fetch(disallowedLocationsUri, { method: "POST", body: JSON.stringify({ resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces], }), headers: { [authorizationHeader.header]: authorizationHeader.token, [Constants.HttpHeaders.contentType]: "application/json", }, }); if (!response.ok) { throw new Error("Failed to fetch disallowed locations"); } const disallowedLocations: string[] = await response.json(); if (!disallowedLocations) { Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount"); set({ isNotebooksEnabledForAccount: true }); return; } // firstWriteLocation should not be disallowed const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1; set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation }); } catch (error) { Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount"); set({ isNotebooksEnabledForAccount: false }); } }, findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => { const currentItem = root || get().myNotebooksContentRoot; if (currentItem) { if (currentItem.path === item.path && currentItem.name === item.name) { return currentItem; } if (currentItem.children) { for (const childItem of currentItem.children) { const result = get().findItem(childItem, item); if (result) { return result; } } } } return undefined; }, insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => { const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, parent); item.parent = parentItem; if (parentItem.children) { parentItem.children.push(item); } else { parentItem.children = [item]; } isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, item.parent); parentItem.children = parentItem.children.filter((child) => child.path !== item.path); parentItem.children.push(item); item.parent = parentItem; isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, item.parent); parentItem.children = parentItem.children.filter((child) => child.path !== item.path); isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }, initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks"; set({ notebookFolderName }); const myNotebooksContentRoot = { name: get().notebookFolderName, path: get().notebookBasePath, type: NotebookContentItemType.Directory, }; const galleryContentRoot = { name: "Gallery", path: "Gallery", type: NotebookContentItemType.File, }; const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn() ? { name: "GitHub repos", path: "PsuedoDir", type: NotebookContentItemType.Directory, } : undefined; set({ myNotebooksContentRoot, galleryContentRoot, gitHubNotebooksContentRoot, }); if (get().notebookServerInfo?.notebookServerEndpoint) { const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot); set({ myNotebooksContentRoot: updatedRoot }); if (updatedRoot?.children) { // Count 1st generation children (tree is lazy-loaded) const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; updatedRoot.children.forEach((notebookItem) => { switch (notebookItem.type) { case NotebookContentItemType.File: nodeCounts.files++; break; case NotebookContentItemType.Directory: nodeCounts.directories++; break; case NotebookContentItemType.Notebook: nodeCounts.notebooks++; break; default: break; } }); TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts }); } } }, initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => { const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot); if (gitHubNotebooksContentRoot) { gitHubNotebooksContentRoot.children = []; pinnedRepos?.forEach((pinnedRepo) => { const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); const repoTreeItem: NotebookContentItem = { name: repoFullName, path: "PsuedoDir", type: NotebookContentItemType.Directory, children: [], parent: gitHubNotebooksContentRoot, }; pinnedRepo.branches.forEach((branch) => { repoTreeItem.children.push({ name: branch.name, path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), type: NotebookContentItemType.Directory, parent: repoTreeItem, }); }); gitHubNotebooksContentRoot.children.push(repoTreeItem); }); set({ gitHubNotebooksContentRoot }); } }, 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().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) { let isPhoenix = false; if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) { const phoenixClient = new PhoenixClient(); isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted()); } const isPhoenixNotebooks = userContext.features.phoenixNotebooks && isPhoenix; const isPhoenixFeatures = userContext.features.phoenixFeatures && isPhoenix; set({ isPhoenixNotebooks: isPhoenixNotebooks }); set({ isPhoenixFeatures: isPhoenixFeatures }); } }, setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }), setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }), }));