diff --git a/less/tree.less b/less/tree.less index 56a5ee38e..8b2c2f1bb 100644 --- a/less/tree.less +++ b/less/tree.less @@ -7,6 +7,7 @@ .main { height: 100%; } + border-right: 1px solid @BaseMedium; } .resourceTreeScroll { diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 722d1990e..900a2008b 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -45,7 +45,7 @@ import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane"; import { ExplorerMetrics } from "../Common/Constants"; import { ExplorerSettings } from "../Shared/ExplorerSettings"; import { FileSystemUtil } from "./Notebook/FileSystemUtil"; -import { IGalleryItem } from "../Juno/JunoClient"; +import { IGalleryItem, IPinnedRepo } from "../Juno/JunoClient"; import { LoadQueryPane } from "./Panes/LoadQueryPane"; import * as Logger from "../Common/Logger"; import { sendMessage, sendCachedDataMessage } from "../Common/MessageHandler"; @@ -103,6 +103,8 @@ export interface ExplorerParams { openDialog: (props: DialogProps) => void; onRefreshNotebookList: () => void; + initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; + getMyNotebooksContentRoot: () => NotebookContentItem; } export default class Explorer { @@ -249,7 +251,7 @@ export default class Explorer { private static readonly MaxNbDatabasesToAutoExpand = 5; - constructor(private params?: ExplorerParams) { + constructor(public params?: ExplorerParams) { this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded; this.setNotificationConsoleData = params?.setNotificationConsoleData; this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted; @@ -1720,7 +1722,7 @@ export default class Explorer { const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); promise - .then(() => this.resourceTree.triggerRender()) + .then(() => this.params.onRefreshNotebookList()) .catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason)); return promise; } @@ -1728,7 +1730,7 @@ export default class Explorer { public async importAndOpen(path: string): Promise { const name = NotebookUtil.getName(path); const item = NotebookUtil.createNotebookContentItem(name, path, "file"); - const parent = this.resourceTree.myNotebooksContentRoot; + const parent = this.params.getMyNotebooksContentRoot(); if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { const existingItem = _.find(parent.children, (node) => node.name === name); @@ -1745,7 +1747,8 @@ export default class Explorer { } public async importAndOpenContent(name: string, content: string): Promise { - const parent = this.resourceTree.myNotebooksContentRoot; + // const parent = this.params.getMyNotebooksContentRoot(); + const parent = this.params.getMyNotebooksContentRoot(); if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { @@ -1930,7 +1933,6 @@ export default class Explorer { return newNotebookFile; }); - result.then(() => this.resourceTree.triggerRender()); return result; } @@ -1951,7 +1953,6 @@ export default class Explorer { defaultInput: "", onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input), }); - result.then(() => this.resourceTree.triggerRender()); return result; } @@ -2113,8 +2114,6 @@ export default class Explorer { return; } - console.log("=======> refreshNotebookList"); - // await this.resourceTree.initialize(); this.params?.onRefreshNotebookList(); this.notebookManager?.refreshPinnedRepos(); @@ -2179,7 +2178,7 @@ export default class Explorer { throw new Error(error); } - parent = parent || this.resourceTree.myNotebooksContentRoot; + parent = parent || this.params.getMyNotebooksContentRoot(); const notificationProgressId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, @@ -2203,7 +2202,7 @@ export default class Explorer { ); return this.openNotebook(newFile); }) - .then(() => this.resourceTree.triggerRender()) + .then(() => this.params.onRefreshNotebookList()) .catch((error: any) => { const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); @@ -2221,7 +2220,7 @@ export default class Explorer { } public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void { - parent = parent || this.resourceTree.myNotebooksContentRoot; + parent = parent || this.params.getMyNotebooksContentRoot(); this.uploadFilePane.openWithOptions({ paneTitle: "Upload file to notebook server", diff --git a/src/Explorer/Notebook/NotebookManager.ts b/src/Explorer/Notebook/NotebookManager.ts index 164d56d58..1c88a891e 100644 --- a/src/Explorer/Notebook/NotebookManager.ts +++ b/src/Explorer/Notebook/NotebookManager.ts @@ -29,7 +29,6 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export interface NotebookManagerOptions { container: Explorer; notebookBasePath: ko.Observable; - // resourceTree: ResourceTreeAdapter; refreshCommandBarButtons: () => void; refreshNotebookList: () => void; } @@ -106,8 +105,8 @@ export default class NotebookManager { }); this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { - this.params.resourceTree.initializeGitHubRepos(pinnedRepos); - this.params.resourceTree.triggerRender(); + // TODO Move this out of NotebookManager? + this.params.container.params.initializeGitHubRepos(pinnedRepos); }); this.refreshPinnedRepos(); } diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index c52dc32db..2dd6a62f8 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -17,10 +17,9 @@ import FileIcon from "../../../images/notebook/file-cosmos.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg"; import { ArrayHashMap } from "../../Common/ArrayHashMap"; import { NotebookUtil } from "../Notebook/NotebookUtil"; -import { IPinnedRepo } from "../../Juno/JunoClient"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; -import { Areas, Notebook } from "../../Common/Constants"; +import { Areas } from "../../Common/Constants"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import GalleryIcon from "../../../images/GalleryIcon.svg"; import { Callout, Text, Link, DirectionalHint, Stack, ICalloutProps, ILinkProps } from "office-ui-fabric-react"; @@ -32,6 +31,7 @@ import Trigger from "./Trigger"; import TabsBase from "../Tabs/TabsBase"; import { userContext } from "../../UserContext"; import * as DataModels from "../../Contracts/DataModels"; +import { DataTitle, NotebooksTitle, PseudoDirPath } from "../../hooks/useNotebooks"; export interface ResourceTreeProps { // TODO remove eventually @@ -39,19 +39,12 @@ export interface ResourceTreeProps { lastRefreshedTime: number; -} - -interface ResourceTreeState { galleryContentRoot: NotebookContentItem; myNotebooksContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem; } -export class ResourceTree extends React.Component { - private static readonly DataTitle = "DATA"; - private static readonly NotebooksTitle = "NOTEBOOKS"; - private static readonly PseudoDirPath = "PseudoDir"; - +export class ResourceTree extends React.Component { private koSubsDatabaseIdMap: ArrayHashMap; // database id -> ko subs private koSubsCollectionIdMap: ArrayHashMap; // collection id -> ko subs private databaseCollectionIdMap: ArrayHashMap; // database id -> collection ids @@ -63,7 +56,7 @@ export class ResourceTree extends React.Component this.watchDatabase(database)); - this.triggerRender(); - } - - private traceMyNotebookTreeInfo() { - const myNotebooksTree = this.state.myNotebooksContentRoot; - if (myNotebooksTree.children) { - // Count 1st generation children (tree is lazy-loaded) - const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; - myNotebooksTree.children.forEach((treeNode) => { - switch ((treeNode as NotebookContentItem).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 }); - } } render(): JSX.Element { @@ -120,15 +88,15 @@ export class ResourceTree extends React.Component - + - + - {this.state.galleryContentRoot && this.buildGalleryCallout()} + {this.props.galleryContentRoot && this.buildGalleryCallout()} ); } else { @@ -136,90 +104,6 @@ export class ResourceTree extends React.Component { - const refreshTasks: Promise[] = []; - - this.setState({ - galleryContentRoot: { - name: "Gallery", - path: "Gallery", - type: NotebookContentItemType.File, - }, - - myNotebooksContentRoot: { - name: Notebook.MyNotebooksTitle, - path: this.container.getNotebookBasePath(), - type: NotebookContentItemType.Directory, - } - }); - - console.log("====> componentDidUpdate"); - // Only if notebook server is available we can refresh - if (this.container.notebookServerInfo().notebookServerEndpoint) { - refreshTasks.push( - this.container.refreshContentItem(this.state.myNotebooksContentRoot).then(root => { - this.setState({ myNotebooksContentRoot: root }); - // this.triggerRender(); - this.traceMyNotebookTreeInfo(); - }) - ); - } - - if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - this.setState({ - gitHubNotebooksContentRoot: { - name: Notebook.GitHubReposTitle, - path: ResourceTree.PseudoDirPath, - type: NotebookContentItemType.Directory, - } - }); - } else { - this.setState({ - gitHubNotebooksContentRoot: undefined - }) - } - - return Promise.all(refreshTasks); - } - - public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void { - if (this.state.gitHubNotebooksContentRoot) { - const { gitHubNotebooksContentRoot } = this.state; - gitHubNotebooksContentRoot.children = []; - this.setState({ gitHubNotebooksContentRoot }); - - pinnedRepos?.forEach((pinnedRepo) => { - const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); - const repoTreeItem: NotebookContentItem = { - name: repoFullName, - path: ResourceTree.PseudoDirPath, - type: NotebookContentItemType.Directory, - children: [], - }; - - pinnedRepo.branches.forEach((branch) => { - repoTreeItem.children.push({ - name: branch.name, - path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), - type: NotebookContentItemType.Directory, - }); - }); - - this.state.gitHubNotebooksContentRoot.children.push(repoTreeItem); - }); - - this.triggerRender(); - } - } - private buildDataTree(): TreeNode { const databaseTreeNodes: TreeNode[] = this.container.nonSystemDatabases().map((database: ViewModels.Database) => { const databaseNode: TreeNode = { @@ -475,8 +359,8 @@ export class ResourceTree extends React.Component { const path: string[] = field.path.split("."); const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let current: any = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let current: any = {}; path.forEach((name: string, pathIndex: number) => { if (pathIndex === 0) { if (schema[name] === undefined) { @@ -526,15 +410,15 @@ export class ResourceTree extends React.Component (node.isExpanded = false)); notebooksTree.children.push(this.buildGitHubNotebooksTree()); @@ -604,7 +488,7 @@ export class ResourceTree extends React.Component { this.container.openNotebook(item).then((hasOpened) => { if (hasOpened) { @@ -625,7 +509,7 @@ export class ResourceTree extends React.Component { this.container.openNotebook(item).then((hasOpened) => { if (hasOpened) { @@ -723,7 +607,7 @@ export class ResourceTree extends React.Component this.container.renameNotebook(item), + onClick: () => this.container.renameNotebook(item).then(() => this.triggerRender()), }, { label: "Delete", @@ -812,7 +696,7 @@ export class ResourceTree extends React.Component this.container.onCreateDirectory(item), + onClick: () => this.container.onCreateDirectory(item).then(() => this.triggerRender()), }, { label: "New Notebook", @@ -870,9 +754,7 @@ export class ResourceTree extends React.Component { }; const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel(); - const {lastRefreshTime, refreshList} = useNotebooks(); + + // TODO Figure out a better pattern: this is because we don't have container, yet + const context: { container: Explorer } = { container: undefined }; + const { + lastRefreshTime, + galleryContentRoot, + myNotebooksContentRoot, + gitHubNotebooksContentRoot, + refreshList, + initializeGitHubRepos, + getMyNotebooksContentRoot, + } = useNotebooks(context); const explorerParams: ExplorerParams = { setIsNotificationConsoleExpanded, @@ -102,19 +113,23 @@ const App: React.FunctionComponent = () => { closeSidePanel, openDialog, closeDialog, - onRefreshNotebookList: refreshList + onRefreshNotebookList: refreshList, + initializeGitHubRepos, + getMyNotebooksContentRoot, }; const config = useConfig(); const explorer = useKnockoutExplorer(config?.platform, explorerParams); -// const [databases, setDatabases] = useState(); -// useEffect(() => { -// fetchDatabases().then((dbs) => { -// setDatabases(dbs) -// explorer.databases(dbs) -// }); -// const databases = useDatabases(explorer) + // TODO fix this + context.container = explorer; + // const [databases, setDatabases] = useState(); + // useEffect(() => { + // fetchDatabases().then((dbs) => { + // setDatabases(dbs) + // explorer.databases(dbs) + // }); + // const databases = useDatabases(explorer) return (
@@ -181,7 +196,13 @@ const App: React.FunctionComponent = () => { data-bind="if: isAuthWithResourceToken(), react:resourceTreeForResourceToken" />
- +
{/* Collections Window - End */} diff --git a/src/hooks/useNotebooks.ts b/src/hooks/useNotebooks.ts index 816300668..43b4decd4 100644 --- a/src/hooks/useNotebooks.ts +++ b/src/hooks/useNotebooks.ts @@ -1,16 +1,149 @@ import { useState } from "react"; +import { Notebook } from "../Common/Constants"; +import Explorer from "../Explorer/Explorer"; +import { NotebookContentItem, NotebookContentItemType } from "../Explorer/Notebook/NotebookContentItem"; +import { IPinnedRepo } from "../Juno/JunoClient"; +import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; +import * as GitHubUtils from "../Utils/GitHubUtils"; + +export const DataTitle = "DATA"; +export const NotebooksTitle = "NOTEBOOKS"; +export const PseudoDirPath = "PseudoDir"; export interface NotebookHooks { lastRefreshTime: number; + galleryContentRoot: NotebookContentItem; + myNotebooksContentRoot: NotebookContentItem; + gitHubNotebooksContentRoot: NotebookContentItem; + refreshList: () => void; + initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; + getMyNotebooksContentRoot: () => NotebookContentItem; } -export const useNotebooks = (): NotebookHooks => { +export const useNotebooks = (context: { container: Explorer }): NotebookHooks => { const [lastRefreshTime, setLastRefreshTime] = useState(undefined); + const [galleryContentRoot, setGalleryContentRoot] = useState(undefined); + const [myNotebooksContentRoot, setMyNotebooksContentRoot] = useState(undefined); + const [gitHubNotebooksContentRoot, setGitHubNotebooksContentRoot] = useState(undefined); const refreshList = (): void => { + initialize(); setLastRefreshTime(new Date().getTime()); - } + }; - return { lastRefreshTime, refreshList }; + // TODO For now, we need to rely on this, as setMyNotebooksContentRoot() is not synchronous + let _myNotebooksContentRoot: NotebookContentItem = undefined; + const _setMyNotebooksContentRoot = (newValue: NotebookContentItem) => { + _myNotebooksContentRoot = newValue; + setMyNotebooksContentRoot(newValue); + }; + + const initialize = (): Promise => { + const refreshTasks: Promise[] = []; + + setGalleryContentRoot({ + name: "Gallery", + path: "Gallery", + type: NotebookContentItemType.File, + }); + + const _myNotebooksContentRoot = { + name: Notebook.MyNotebooksTitle, + path: context.container.getNotebookBasePath(), + type: NotebookContentItemType.Directory, + }; + _setMyNotebooksContentRoot(_myNotebooksContentRoot); + + // Only if notebook server is available we can refresh + if (context.container.notebookServerInfo().notebookServerEndpoint) { + refreshTasks.push( + context.container.refreshContentItem(_myNotebooksContentRoot).then((root) => { + _setMyNotebooksContentRoot({ ...root }); + traceMyNotebookTreeInfo(root); + }) + ); + } + + initializeGitHubNotebooksContentRoot(); + return Promise.all(refreshTasks); + }; + + const traceMyNotebookTreeInfo = (myNotebooksTree: NotebookContentItem) => { + if (myNotebooksTree.children) { + // Count 1st generation children (tree is lazy-loaded) + const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; + myNotebooksTree.children.forEach((treeNode) => { + switch ((treeNode as NotebookContentItem).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 }); + } + }; + + const initializeGitHubNotebooksContentRoot = (): NotebookContentItem => { + let root: NotebookContentItem = undefined; + + if (context.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { + root = { + name: Notebook.GitHubReposTitle, + path: PseudoDirPath, + type: NotebookContentItemType.Directory, + }; + } + setGitHubNotebooksContentRoot(root); + return root; + }; + + const initializeGitHubRepos = (pinnedRepos: IPinnedRepo[]): void => { + const _gitHubNotebooksContentRoot = initializeGitHubNotebooksContentRoot(); + + if (_gitHubNotebooksContentRoot) { + _gitHubNotebooksContentRoot.children = []; + + pinnedRepos?.forEach((pinnedRepo) => { + const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); + const repoTreeItem: NotebookContentItem = { + name: repoFullName, + path: PseudoDirPath, + type: NotebookContentItemType.Directory, + children: [], + }; + + pinnedRepo.branches.forEach((branch) => { + repoTreeItem.children.push({ + name: branch.name, + path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), + type: NotebookContentItemType.Directory, + }); + }); + + _gitHubNotebooksContentRoot.children.push(repoTreeItem); + }); + + setGitHubNotebooksContentRoot({ ..._gitHubNotebooksContentRoot }); + } + }; + + return { + lastRefreshTime, + galleryContentRoot, + myNotebooksContentRoot, + gitHubNotebooksContentRoot, + refreshList, + initializeGitHubRepos, + getMyNotebooksContentRoot: () => _myNotebooksContentRoot, + }; };