diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 2f831b77e..e3262262e 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -392,6 +392,9 @@ export class Notebook { public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartMaxDelayMs = 20000; public static readonly autoSaveIntervalMs = 120000; + + public static readonly MyNotebooksTitle = "My Notebooks"; + public static readonly GitHubReposTitle = "GitHub repos"; } export class SparkLibrary { diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 8254ddf42..722d1990e 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -56,7 +56,6 @@ import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { QueriesClient } from "../Common/QueriesClient"; import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; -import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import { RouteHandler } from "../RouteHandlers/RouteHandler"; import { SaveQueryPane } from "./Panes/SaveQueryPane"; @@ -102,6 +101,8 @@ export interface ExplorerParams { closeSidePanel: () => void; closeDialog: () => void; openDialog: (props: DialogProps) => void; + + onRefreshNotebookList: () => void; } export default class Explorer { @@ -160,7 +161,6 @@ export default class Explorer { public isLeftPaneExpanded: ko.Observable; public selectedNode: ko.Observable; public isRefreshingExplorer: ko.Observable; - private resourceTree: ResourceTreeAdapter; private selfServeComponentAdapter: SelfServeComponentAdapter; // Resource Token @@ -249,7 +249,7 @@ export default class Explorer { private static readonly MaxNbDatabasesToAutoExpand = 5; - constructor(params?: ExplorerParams) { + constructor(private params?: ExplorerParams) { this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded; this.setNotificationConsoleData = params?.setNotificationConsoleData; this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted; @@ -859,7 +859,6 @@ export default class Explorer { this.notebookManager.initialize({ container: this, notebookBasePath: this.notebookBasePath, - resourceTree: this.resourceTree, refreshCommandBarButtons: () => this.refreshCommandBarButtons(), refreshNotebookList: () => this.refreshNotebookList(), }); @@ -874,7 +873,6 @@ export default class Explorer { this.isSparkEnabled = ko.observable(false); this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons()); - this.resourceTree = new ResourceTreeAdapter(this); this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this); this.notebookServerInfo = ko.observable({ notebookServerEndpoint: undefined, @@ -2109,12 +2107,16 @@ export default class Explorer { return false; } }; + private refreshNotebookList = async (): Promise => { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { return; } - await this.resourceTree.initialize(); + console.log("=======> refreshNotebookList"); + // await this.resourceTree.initialize(); + this.params?.onRefreshNotebookList(); + this.notebookManager?.refreshPinnedRepos(); if (this.notebookToImport) { this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); @@ -2250,7 +2252,7 @@ export default class Explorer { }); } - public refreshContentItem(item: NotebookContentItem): Promise { + public refreshContentItem(item: NotebookContentItem): Promise { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { const error = "Attempt to refresh notebook list, but notebook is not enabled"; handleError(error, "Explorer/refreshContentItem"); diff --git a/src/Explorer/Notebook/NotebookContentClient.ts b/src/Explorer/Notebook/NotebookContentClient.ts index dfb5ab8c5..8a061ed7e 100644 --- a/src/Explorer/Notebook/NotebookContentClient.ts +++ b/src/Explorer/Notebook/NotebookContentClient.ts @@ -18,11 +18,13 @@ export class NotebookContentClient { /** * This updates the item and points all the children's parent to this item * @param item + * @return updated item */ - public updateItemChildren(item: NotebookContentItem): Promise { + public updateItemChildren(item: NotebookContentItem): Promise { return this.fetchNotebookFiles(item.path).then((subItems) => { item.children = subItems; subItems.forEach((subItem) => (subItem.parent = item)); + return item; }); } diff --git a/src/Explorer/Notebook/NotebookManager.ts b/src/Explorer/Notebook/NotebookManager.ts index b59c3377b..164d56d58 100644 --- a/src/Explorer/Notebook/NotebookManager.ts +++ b/src/Explorer/Notebook/NotebookManager.ts @@ -18,7 +18,6 @@ import { contents } from "rx-jupyter"; import { NotebookContainerClient } from "./NotebookContainerClient"; import { MemoryUsageInfo } from "../../Contracts/DataModels"; import { NotebookContentClient } from "./NotebookContentClient"; -import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter"; import { getFullName } from "../../Utils/UserUtils"; import { ImmutableNotebook } from "@nteract/commutable"; @@ -30,7 +29,7 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export interface NotebookManagerOptions { container: Explorer; notebookBasePath: ko.Observable; - resourceTree: ResourceTreeAdapter; + // resourceTree: ResourceTreeAdapter; refreshCommandBarButtons: () => void; refreshNotebookList: () => void; } diff --git a/src/Explorer/Panes/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane.tsx index 8cc0cdafb..cb9c2766f 100644 --- a/src/Explorer/Panes/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane.tsx @@ -8,10 +8,9 @@ import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRight import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; import { IDropdownOption } from "office-ui-fabric-react"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; -import { HttpStatusCodes } from "../../Common/Constants"; +import { HttpStatusCodes, Notebook } from "../../Common/Constants"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem"; -import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils"; interface Location { @@ -151,7 +150,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter { switch (location.type) { case "MyNotebooks": parent = { - name: ResourceTreeAdapter.MyNotebooksTitle, + name: Notebook.MyNotebooksTitle, path: this.container.getNotebookBasePath(), type: NotebookContentItemType.Directory, }; @@ -159,7 +158,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter { case "GitHub": parent = { - name: ResourceTreeAdapter.GitHubReposTitle, + name: Notebook.GitHubReposTitle, path: GitHubUtils.toContentUri( this.selectedLocation.owner, this.selectedLocation.repo, diff --git a/src/Explorer/Panes/CopyNotebookPaneComponent.tsx b/src/Explorer/Panes/CopyNotebookPaneComponent.tsx index 7ae30ccfe..9ef88addb 100644 --- a/src/Explorer/Panes/CopyNotebookPaneComponent.tsx +++ b/src/Explorer/Panes/CopyNotebookPaneComponent.tsx @@ -1,7 +1,6 @@ import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as React from "react"; import { IPinnedRepo } from "../../Juno/JunoClient"; -import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { Stack, Label, @@ -13,6 +12,7 @@ import { IRenderFunction, ISelectableOption, } from "office-ui-fabric-react"; +import { Notebook } from "../../Common/Constants"; interface Location { type: "MyNotebooks" | "GitHub"; @@ -70,8 +70,8 @@ export class CopyNotebookPaneComponent extends React.Component { private static readonly DataTitle = "DATA"; private static readonly NotebooksTitle = "NOTEBOOKS"; - private static readonly PseudoDirPath = "PsuedoDir"; - - public parameters: ko.Observable; - - public galleryContentRoot: NotebookContentItem; - public myNotebooksContentRoot: NotebookContentItem; - public gitHubNotebooksContentRoot: NotebookContentItem; + private static readonly PseudoDirPath = "PseudoDir"; private koSubsDatabaseIdMap: ArrayHashMap; // database id -> ko subs private koSubsCollectionIdMap: ArrayHashMap; // collection id -> ko subs private databaseCollectionIdMap: ArrayHashMap; // database id -> collection ids - public constructor(private container: Explorer) { - this.parameters = ko.observable(Date.now()); + private readonly container: Explorer; - this.container.selectedNode.subscribe((newValue: any) => this.triggerRender()); - this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender()); - this.container.isNotebookEnabled.subscribe((newValue) => this.triggerRender()); + constructor(props: ResourceTreeProps) { + super(props); + this.state = { + galleryContentRoot: undefined, + myNotebooksContentRoot: undefined, + gitHubNotebooksContentRoot: undefined + }; + + this.container = props.explorer; + + this.container.selectedNode.subscribe(() => this.triggerRender()); + this.container.tabsManager.activeTab.subscribe(() => this.triggerRender()); + this.container.isNotebookEnabled.subscribe(() => this.triggerRender()); this.koSubsDatabaseIdMap = new ArrayHashMap(); this.koSubsCollectionIdMap = new ArrayHashMap(); @@ -77,7 +89,7 @@ export class ResourceTreeAdapter implements ReactAdapter { } private traceMyNotebookTreeInfo() { - const myNotebooksTree = this.myNotebooksContentRoot; + const myNotebooksTree = this.state.myNotebooksContentRoot; if (myNotebooksTree.children) { // Count 1st generation children (tree is lazy-loaded) const nodeCounts = { files: 0, notebooks: 0, directories: 0 }; @@ -100,7 +112,7 @@ export class ResourceTreeAdapter implements ReactAdapter { } } - public renderComponent(): JSX.Element { + render(): JSX.Element { const dataRootNode = this.buildDataTree(); const notebooksRootNode = this.buildNotebooksTrees(); @@ -108,15 +120,15 @@ export class ResourceTreeAdapter implements ReactAdapter { return ( <> - + - + - {this.galleryContentRoot && this.buildGalleryCallout()} + {this.state.galleryContentRoot && this.buildGalleryCallout()} ); } else { @@ -124,52 +136,71 @@ export class ResourceTreeAdapter implements ReactAdapter { } } - public async initialize(): Promise { + componentDidUpdate(prevProps: ResourceTreeProps): void { + if (this.props.lastRefreshedTime === undefined || prevProps.lastRefreshedTime === this.props.lastRefreshedTime) { + return; + } + + this.initialize(); + } + + private async initialize(): Promise { const refreshTasks: Promise[] = []; - this.galleryContentRoot = { - name: "Gallery", - path: "Gallery", - type: NotebookContentItemType.File, - }; + this.setState({ + galleryContentRoot: { + name: "Gallery", + path: "Gallery", + type: NotebookContentItemType.File, + }, - this.myNotebooksContentRoot = { - name: ResourceTreeAdapter.MyNotebooksTitle, - path: this.container.getNotebookBasePath(), - type: NotebookContentItemType.Directory, - }; + 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.myNotebooksContentRoot).then(() => { - this.triggerRender(); + this.container.refreshContentItem(this.state.myNotebooksContentRoot).then(root => { + this.setState({ myNotebooksContentRoot: root }); + // this.triggerRender(); this.traceMyNotebookTreeInfo(); }) ); } if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { - this.gitHubNotebooksContentRoot = { - name: ResourceTreeAdapter.GitHubReposTitle, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - }; + this.setState({ + gitHubNotebooksContentRoot: { + name: Notebook.GitHubReposTitle, + path: ResourceTree.PseudoDirPath, + type: NotebookContentItemType.Directory, + } + }); } else { - this.gitHubNotebooksContentRoot = undefined; + this.setState({ + gitHubNotebooksContentRoot: undefined + }) } return Promise.all(refreshTasks); } public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void { - if (this.gitHubNotebooksContentRoot) { - this.gitHubNotebooksContentRoot.children = []; + 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: ResourceTreeAdapter.PseudoDirPath, + path: ResourceTree.PseudoDirPath, type: NotebookContentItemType.Directory, children: [], }; @@ -182,7 +213,7 @@ export class ResourceTreeAdapter implements ReactAdapter { }); }); - this.gitHubNotebooksContentRoot.children.push(repoTreeItem); + this.state.gitHubNotebooksContentRoot.children.push(repoTreeItem); }); this.triggerRender(); @@ -296,7 +327,7 @@ export class ResourceTreeAdapter implements ReactAdapter { children.push(schemaNode); } - if (ResourceTreeAdapter.showScriptNodes(this.container)) { + if (ResourceTree.showScriptNodes(this.container)) { children.push(this.buildStoredProcedureNode(collection)); children.push(this.buildUserDefinedFunctionsNode(collection)); children.push(this.buildTriggerNode(collection)); @@ -337,7 +368,7 @@ export class ResourceTreeAdapter implements ReactAdapter { ); }, onExpanded: () => { - if (ResourceTreeAdapter.showScriptNodes(this.container)) { + if (ResourceTree.showScriptNodes(this.container)) { collection.loadStoredProcedures(); collection.loadUserDefinedFunctions(); collection.loadTriggers(); @@ -416,7 +447,7 @@ export class ResourceTreeAdapter implements ReactAdapter { } public buildSchemaNode(collection: ViewModels.Collection): TreeNode { - if (collection.analyticalStorageTtl() == undefined) { + if (collection.analyticalStorageTtl() === undefined) { return undefined; } @@ -437,13 +468,15 @@ export class ResourceTreeAdapter implements ReactAdapter { } private getSchemaNodes(fields: DataModels.IDataField[]): TreeNode[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const schema: any = {}; //unflatten - fields.forEach((field: DataModels.IDataField, fieldIndex: number) => { + fields.forEach((field: DataModels.IDataField) => { const path: string[] = field.path.split("."); const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`]; - 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) { @@ -467,9 +500,11 @@ export class ResourceTreeAdapter implements ReactAdapter { }); }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const traverse = (obj: any): TreeNode[] => { const children: TreeNode[] = []; + // eslint-disable-next-line no-null/no-null if (obj !== null && !Array.isArray(obj) && typeof obj === "object") { Object.entries(obj).forEach(([key, value]) => { children.push({ label: key, children: traverse(value) }); @@ -485,21 +520,21 @@ export class ResourceTreeAdapter implements ReactAdapter { } private buildNotebooksTrees(): TreeNode { - let notebooksTree: TreeNode = { + const notebooksTree: TreeNode = { label: undefined, isExpanded: true, children: [], }; - if (this.galleryContentRoot) { + if (this.state.galleryContentRoot) { notebooksTree.children.push(this.buildGalleryNotebooksTree()); } - if (this.myNotebooksContentRoot) { + if (this.state.myNotebooksContentRoot) { notebooksTree.children.push(this.buildMyNotebooksTree()); } - if (this.gitHubNotebooksContentRoot) { + if (this.state.gitHubNotebooksContentRoot) { // collapse all other notebook nodes notebooksTree.children.forEach((node) => (node.isExpanded = false)); notebooksTree.children.push(this.buildGitHubNotebooksTree()); @@ -569,7 +604,7 @@ export class ResourceTreeAdapter implements ReactAdapter { private buildMyNotebooksTree(): TreeNode { const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.myNotebooksContentRoot, + this.state.myNotebooksContentRoot, (item: NotebookContentItem) => { this.container.openNotebook(item).then((hasOpened) => { if (hasOpened) { @@ -590,7 +625,7 @@ export class ResourceTreeAdapter implements ReactAdapter { private buildGitHubNotebooksTree(): TreeNode { const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( - this.gitHubNotebooksContentRoot, + this.state.gitHubNotebooksContentRoot, (item: NotebookContentItem) => { this.container.openNotebook(item).then((hasOpened) => { if (hasOpened) { @@ -674,6 +709,7 @@ export class ResourceTreeAdapter implements ReactAdapter { /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any (activeTab as any).notebookPath() === item.path ); }, @@ -829,11 +865,12 @@ export class ResourceTreeAdapter implements ReactAdapter { /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. NotebookV2Tab could be dynamically imported, but not worth it to just get this type right. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any (activeTab as any).notebookPath() === item.path ); }, contextMenu: - createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath + createDirectoryContextMenu && item.path !== ResourceTree.PseudoDirPath ? this.createDirectoryContextMenu(item) : undefined, data: item, @@ -841,8 +878,8 @@ export class ResourceTreeAdapter implements ReactAdapter { }; } - public triggerRender() { - window.requestAnimationFrame(() => this.parameters(Date.now())); + private triggerRender() { + this.setState({}); } /** diff --git a/src/Explorer/Tree/ResourceTreeAdapter.test.ts b/src/Explorer/Tree/ResourceTreeAdapter.test.ts index 5102149c1..3be998533 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.test.ts +++ b/src/Explorer/Tree/ResourceTreeAdapter.test.ts @@ -1,6 +1,6 @@ import Explorer from "../Explorer"; import * as ko from "knockout"; -import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; +import { ResourceTreeAdapter } from "./ResourceTree"; import * as ViewModels from "../../Contracts/ViewModels"; import TabsBase from "../Tabs/TabsBase"; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.test.tsx b/src/Explorer/Tree/ResourceTreeAdapter.test.tsx index a161eaf1d..e73108da0 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.test.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.test.tsx @@ -2,7 +2,7 @@ import * as ko from "knockout"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import React from "react"; -import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; +import { ResourceTreeAdapter } from "./ResourceTree"; import { shallow } from "enzyme"; import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; diff --git a/src/Main.tsx b/src/Main.tsx index 1a00f2a09..3fd91d70e 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -69,6 +69,8 @@ import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConso import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent"; import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen"; import { Dialog, DialogProps } from "./Explorer/Controls/Dialog"; +import { ResourceTree } from "./Explorer/Tree/ResourceTree"; +import { useNotebooks } from "./hooks/useNotebooks"; initializeIcons(); @@ -90,6 +92,7 @@ const App: React.FunctionComponent = () => { }; const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel(); + const {lastRefreshTime, refreshList} = useNotebooks(); const explorerParams: ExplorerParams = { setIsNotificationConsoleExpanded, @@ -99,10 +102,20 @@ const App: React.FunctionComponent = () => { closeSidePanel, openDialog, closeDialog, + onRefreshNotebookList: refreshList }; 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) + + return (
{ style={{ overflowY: "auto" }} data-bind="if: isAuthWithResourceToken(), react:resourceTreeForResourceToken" /> -
+
+ +
{/* Collections Window - End */}
diff --git a/src/hooks/useNotebooks.ts b/src/hooks/useNotebooks.ts new file mode 100644 index 000000000..816300668 --- /dev/null +++ b/src/hooks/useNotebooks.ts @@ -0,0 +1,16 @@ +import { useState } from "react"; + +export interface NotebookHooks { + lastRefreshTime: number; + refreshList: () => void; +} + +export const useNotebooks = (): NotebookHooks => { + const [lastRefreshTime, setLastRefreshTime] = useState(undefined); + + const refreshList = (): void => { + setLastRefreshTime(new Date().getTime()); + } + + return { lastRefreshTime, refreshList }; +};