Compare commits

...

7 Commits

Author SHA1 Message Date
Laurent Nguyen
a17d71d76e Merge branch 'master' into languy-resource-tree-to-react 2021-03-16 16:27:30 +01:00
Laurent Nguyen
faf2e3b559 Remove comment and test code 2021-03-15 17:35:35 +01:00
Laurent Nguyen
7e992c2b17 Fix build issues and reformat 2021-03-12 14:01:35 +01:00
Laurent Nguyen
21b92ed4f8 Merge branch 'master' into languy-resource-tree-to-react 2021-03-12 11:21:01 +01:00
Laurent Nguyen
e5755dff39 Fix unit tests 2021-03-12 10:36:27 +01:00
Laurent Nguyen
e48a6a10cb Fix notebook updates issues 2021-03-11 16:22:59 +01:00
Laurent Nguyen
4480a7250d Remove ResourceTreeAdapter 2021-03-08 14:20:27 +01:00
13 changed files with 321 additions and 258 deletions

View File

@@ -7,6 +7,7 @@
.main { .main {
height: 100%; height: 100%;
} }
border-right: 1px solid @BaseMedium;
} }
.resourceTreeScroll { .resourceTreeScroll {

View File

@@ -392,6 +392,9 @@ export class Notebook {
public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000; public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000; public static readonly autoSaveIntervalMs = 120000;
public static readonly MyNotebooksTitle = "My Notebooks";
public static readonly GitHubReposTitle = "GitHub repos";
} }
export class SparkLibrary { export class SparkLibrary {

View File

@@ -986,6 +986,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"openDialog": undefined, "openDialog": undefined,
"openSidePanel": undefined, "openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -1018,26 +1019,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function], "resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -2187,6 +2168,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"openDialog": undefined, "openDialog": undefined,
"openSidePanel": undefined, "openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -2219,26 +2201,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function], "resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -3401,6 +3363,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"openDialog": undefined, "openDialog": undefined,
"openSidePanel": undefined, "openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -3433,26 +3396,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function], "resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -4602,6 +4545,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"openDialog": undefined, "openDialog": undefined,
"openSidePanel": undefined, "openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -4634,26 +4578,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function], "resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [Function], "resourceTokenDatabaseId": [Function],
"resourceTokenPartitionKey": [Function], "resourceTokenPartitionKey": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],

View File

@@ -21,7 +21,7 @@ import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType"; import { SubscriptionType } from "../Contracts/SubscriptionType";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { IGalleryItem } from "../Juno/JunoClient"; import { IGalleryItem, IPinnedRepo } from "../Juno/JunoClient";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { RouteHandler } from "../RouteHandlers/RouteHandler"; import { RouteHandler } from "../RouteHandlers/RouteHandler";
@@ -77,7 +77,6 @@ import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database"; import Database from "./Tree/Database";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger"; import Trigger from "./Tree/Trigger";
@@ -95,6 +94,10 @@ export interface ExplorerParams {
closeSidePanel: () => void; closeSidePanel: () => void;
closeDialog: () => void; closeDialog: () => void;
openDialog: (props: DialogProps) => void; openDialog: (props: DialogProps) => void;
onRefreshNotebookList: () => void;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
getMyNotebooksContentRoot: () => NotebookContentItem;
} }
export default class Explorer { export default class Explorer {
@@ -191,7 +194,6 @@ export default class Explorer {
* Use a local loading state and spinner instead. Using a global isRefreshing state causes problems. * Use a local loading state and spinner instead. Using a global isRefreshing state causes problems.
* */ * */
public isRefreshingExplorer: ko.Observable<boolean>; public isRefreshingExplorer: ko.Observable<boolean>;
private resourceTree: ResourceTreeAdapter;
// Resource Token // Resource Token
public resourceTokenDatabaseId: ko.Observable<string>; public resourceTokenDatabaseId: ko.Observable<string>;
@@ -278,7 +280,7 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor(params?: ExplorerParams) { constructor(public params?: ExplorerParams) {
this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded; this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded;
this.setNotificationConsoleData = params?.setNotificationConsoleData; this.setNotificationConsoleData = params?.setNotificationConsoleData;
this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted; this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted;
@@ -878,7 +880,6 @@ export default class Explorer {
this.notebookManager.initialize({ this.notebookManager.initialize({
container: this, container: this,
notebookBasePath: this.notebookBasePath, notebookBasePath: this.notebookBasePath,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(), refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(), refreshNotebookList: () => this.refreshNotebookList(),
}); });
@@ -893,7 +894,6 @@ export default class Explorer {
this.isSparkEnabled = ko.observable(false); this.isSparkEnabled = ko.observable(false);
this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons()); this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons());
this.resourceTree = new ResourceTreeAdapter(this);
this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this); this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this);
this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({ this.notebookServerInfo = ko.observable<DataModels.NotebookWorkspaceConnectionInfo>({
notebookServerEndpoint: undefined, notebookServerEndpoint: undefined,
@@ -1707,7 +1707,7 @@ export default class Explorer {
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent);
promise promise
.then(() => this.resourceTree.triggerRender()) .then(() => this.params.onRefreshNotebookList())
.catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason)); .catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason));
return promise; return promise;
} }
@@ -1715,7 +1715,7 @@ export default class Explorer {
public async importAndOpen(path: string): Promise<boolean> { public async importAndOpen(path: string): Promise<boolean> {
const name = NotebookUtil.getName(path); const name = NotebookUtil.getName(path);
const item = NotebookUtil.createNotebookContentItem(name, path, "file"); 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) { if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
const existingItem = _.find(parent.children, (node) => node.name === name); const existingItem = _.find(parent.children, (node) => node.name === name);
@@ -1732,7 +1732,8 @@ export default class Explorer {
} }
public async importAndOpenContent(name: string, content: string): Promise<boolean> { public async importAndOpenContent(name: string, content: string): Promise<boolean> {
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 (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) {
if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) {
@@ -1917,7 +1918,6 @@ export default class Explorer {
return newNotebookFile; return newNotebookFile;
}); });
result.then(() => this.resourceTree.triggerRender());
return result; return result;
} }
@@ -1938,7 +1938,6 @@ export default class Explorer {
defaultInput: "", defaultInput: "",
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input), onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input),
}); });
result.then(() => this.resourceTree.triggerRender());
return result; return result;
} }
@@ -2094,12 +2093,14 @@ export default class Explorer {
return false; return false;
} }
}; };
private refreshNotebookList = async (): Promise<void> => { private refreshNotebookList = async (): Promise<void> => {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
return; return;
} }
await this.resourceTree.initialize(); this.params?.onRefreshNotebookList();
this.notebookManager?.refreshPinnedRepos(); this.notebookManager?.refreshPinnedRepos();
if (this.notebookToImport) { if (this.notebookToImport) {
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
@@ -2162,7 +2163,7 @@ export default class Explorer {
throw new Error(error); throw new Error(error);
} }
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.params.getMyNotebooksContentRoot();
const notificationProgressId = NotificationConsoleUtils.logConsoleMessage( const notificationProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
@@ -2186,7 +2187,7 @@ export default class Explorer {
); );
return this.openNotebook(newFile); return this.openNotebook(newFile);
}) })
.then(() => this.resourceTree.triggerRender()) .then(() => this.params.onRefreshNotebookList())
.catch((error: any) => { .catch((error: any) => {
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage);
@@ -2204,7 +2205,7 @@ export default class Explorer {
} }
public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void { public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.params.getMyNotebooksContentRoot();
this.uploadFilePane.openWithOptions({ this.uploadFilePane.openWithOptions({
paneTitle: "Upload file to notebook server", paneTitle: "Upload file to notebook server",
@@ -2235,7 +2236,7 @@ export default class Explorer {
}); });
} }
public refreshContentItem(item: NotebookContentItem): Promise<void> { public refreshContentItem(item: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled"; const error = "Attempt to refresh notebook list, but notebook is not enabled";
handleError(error, "Explorer/refreshContentItem"); handleError(error, "Explorer/refreshContentItem");

View File

@@ -18,11 +18,13 @@ export class NotebookContentClient {
/** /**
* This updates the item and points all the children's parent to this item * This updates the item and points all the children's parent to this item
* @param item * @param item
* @return updated item
*/ */
public updateItemChildren(item: NotebookContentItem): Promise<void> { public updateItemChildren(item: NotebookContentItem): Promise<NotebookContentItem> {
return this.fetchNotebookFiles(item.path).then((subItems) => { return this.fetchNotebookFiles(item.path).then((subItems) => {
item.children = subItems; item.children = subItems;
subItems.forEach((subItem) => (subItem.parent = item)); subItems.forEach((subItem) => (subItem.parent = item));
return item;
}); });
} }

View File

@@ -18,7 +18,6 @@ import { contents } from "rx-jupyter";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContainerClient } from "./NotebookContainerClient";
import { MemoryUsageInfo } from "../../Contracts/DataModels"; import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { NotebookContentClient } from "./NotebookContentClient"; import { NotebookContentClient } from "./NotebookContentClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter"; import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { getFullName } from "../../Utils/UserUtils"; import { getFullName } from "../../Utils/UserUtils";
import { ImmutableNotebook } from "@nteract/commutable"; import { ImmutableNotebook } from "@nteract/commutable";
@@ -30,7 +29,6 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export interface NotebookManagerOptions { export interface NotebookManagerOptions {
container: Explorer; container: Explorer;
notebookBasePath: ko.Observable<string>; notebookBasePath: ko.Observable<string>;
resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void; refreshCommandBarButtons: () => void;
refreshNotebookList: () => void; refreshNotebookList: () => void;
} }
@@ -107,8 +105,8 @@ export default class NotebookManager {
}); });
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos); // TODO Move this out of NotebookManager?
this.params.resourceTree.triggerRender(); this.params.container.params.initializeGitHubRepos(pinnedRepos);
}); });
this.refreshPinnedRepos(); this.refreshPinnedRepos();
} }

View File

@@ -8,10 +8,9 @@ import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRight
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
import { IDropdownOption } from "office-ui-fabric-react"; import { IDropdownOption } from "office-ui-fabric-react";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { HttpStatusCodes } from "../../Common/Constants"; import { HttpStatusCodes, Notebook } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils"; import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
interface Location { interface Location {
@@ -151,7 +150,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
switch (location.type) { switch (location.type) {
case "MyNotebooks": case "MyNotebooks":
parent = { parent = {
name: ResourceTreeAdapter.MyNotebooksTitle, name: Notebook.MyNotebooksTitle,
path: this.container.getNotebookBasePath(), path: this.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
@@ -159,7 +158,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
case "GitHub": case "GitHub":
parent = { parent = {
name: ResourceTreeAdapter.GitHubReposTitle, name: Notebook.GitHubReposTitle,
path: GitHubUtils.toContentUri( path: GitHubUtils.toContentUri(
this.selectedLocation.owner, this.selectedLocation.owner,
this.selectedLocation.repo, this.selectedLocation.repo,

View File

@@ -1,7 +1,6 @@
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as React from "react"; import * as React from "react";
import { IPinnedRepo } from "../../Juno/JunoClient"; import { IPinnedRepo } from "../../Juno/JunoClient";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { import {
Stack, Stack,
Label, Label,
@@ -13,6 +12,7 @@ import {
IRenderFunction, IRenderFunction,
ISelectableOption, ISelectableOption,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import { Notebook } from "../../Common/Constants";
interface Location { interface Location {
type: "MyNotebooks" | "GitHub"; type: "MyNotebooks" | "GitHub";
@@ -70,8 +70,8 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
options.push({ options.push({
key: "MyNotebooks-Item", key: "MyNotebooks-Item",
text: ResourceTreeAdapter.MyNotebooksTitle, text: Notebook.MyNotebooksTitle,
title: ResourceTreeAdapter.MyNotebooksTitle, title: Notebook.MyNotebooksTitle,
data: { data: {
type: "MyNotebooks", type: "MyNotebooks",
} as Location, } as Location,
@@ -86,7 +86,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
options.push({ options.push({
key: "GitHub-Header", key: "GitHub-Header",
text: ResourceTreeAdapter.GitHubReposTitle, text: Notebook.GitHubReposTitle,
itemType: SelectableOptionMenuItemType.Header, itemType: SelectableOptionMenuItemType.Header,
}); });

View File

@@ -1,8 +1,7 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as React from "react"; import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory"; import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
@@ -18,8 +17,6 @@ import FileIcon from "../../../images/notebook/file-cosmos.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg"; import PublishIcon from "../../../images/notebook/publish_content.svg";
import { ArrayHashMap } from "../../Common/ArrayHashMap"; import { ArrayHashMap } from "../../Common/ArrayHashMap";
import { NotebookUtil } from "../Notebook/NotebookUtil"; import { NotebookUtil } from "../Notebook/NotebookUtil";
import _ from "underscore";
import { IPinnedRepo } from "../../Juno/JunoClient";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../Common/Constants"; import { Areas } from "../../Common/Constants";
@@ -34,31 +31,39 @@ import Trigger from "./Trigger";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { DataTitle, NotebooksTitle, PseudoDirPath } from "../../hooks/useNotebooks";
export class ResourceTreeAdapter implements ReactAdapter { export interface ResourceTreeProps {
public static readonly MyNotebooksTitle = "My Notebooks"; // TODO remove eventually
public static readonly GitHubReposTitle = "GitHub repos"; explorer: Explorer;
private static readonly DataTitle = "DATA"; lastRefreshedTime: number;
private static readonly NotebooksTitle = "NOTEBOOKS";
private static readonly PseudoDirPath = "PsuedoDir";
public parameters: ko.Observable<number>; galleryContentRoot: NotebookContentItem;
myNotebooksContentRoot: NotebookContentItem;
public galleryContentRoot: NotebookContentItem; gitHubNotebooksContentRoot: NotebookContentItem;
public myNotebooksContentRoot: NotebookContentItem; }
public gitHubNotebooksContentRoot: NotebookContentItem;
export class ResourceTree extends React.Component<ResourceTreeProps> {
private koSubsDatabaseIdMap: ArrayHashMap<ko.Subscription>; // database id -> ko subs private koSubsDatabaseIdMap: ArrayHashMap<ko.Subscription>; // database id -> ko subs
private koSubsCollectionIdMap: ArrayHashMap<ko.Subscription>; // collection id -> ko subs private koSubsCollectionIdMap: ArrayHashMap<ko.Subscription>; // collection id -> ko subs
private databaseCollectionIdMap: ArrayHashMap<string>; // database id -> collection ids private databaseCollectionIdMap: ArrayHashMap<string>; // database id -> collection ids
public constructor(private container: Explorer) { private readonly container: Explorer;
this.parameters = ko.observable(Date.now());
this.container.selectedNode.subscribe((newValue: any) => this.triggerRender()); constructor(props: ResourceTreeProps) {
this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender()); super(props);
this.container.isNotebookEnabled.subscribe((newValue) => this.triggerRender()); 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.koSubsDatabaseIdMap = new ArrayHashMap();
this.koSubsCollectionIdMap = new ArrayHashMap(); this.koSubsCollectionIdMap = new ArrayHashMap();
@@ -73,34 +78,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
}); });
this.container.nonSystemDatabases().forEach((database: ViewModels.Database) => this.watchDatabase(database)); this.container.nonSystemDatabases().forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.triggerRender();
} }
private traceMyNotebookTreeInfo() { render(): JSX.Element {
const myNotebooksTree = this.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 });
}
}
public renderComponent(): JSX.Element {
const dataRootNode = this.buildDataTree(); const dataRootNode = this.buildDataTree();
const notebooksRootNode = this.buildNotebooksTrees(); const notebooksRootNode = this.buildNotebooksTrees();
@@ -108,15 +88,15 @@ export class ResourceTreeAdapter implements ReactAdapter {
return ( return (
<> <>
<AccordionComponent> <AccordionComponent>
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}> <AccordionItemComponent title={DataTitle} isExpanded={!this.props.gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} /> <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent> </AccordionItemComponent>
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}> <AccordionItemComponent title={NotebooksTitle}>
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} /> <TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
</AccordionItemComponent> </AccordionItemComponent>
</AccordionComponent> </AccordionComponent>
{this.galleryContentRoot && this.buildGalleryCallout()} {this.props.galleryContentRoot && this.buildGalleryCallout()}
</> </>
); );
} else { } else {
@@ -124,71 +104,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
} }
} }
public async initialize(): Promise<void[]> {
const refreshTasks: Promise<void>[] = [];
this.galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
this.myNotebooksContentRoot = {
name: ResourceTreeAdapter.MyNotebooksTitle,
path: this.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory,
};
// 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.traceMyNotebookTreeInfo();
})
);
}
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
this.gitHubNotebooksContentRoot = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: ResourceTreeAdapter.PseudoDirPath,
type: NotebookContentItemType.Directory,
};
} else {
this.gitHubNotebooksContentRoot = undefined;
}
return Promise.all(refreshTasks);
}
public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void {
if (this.gitHubNotebooksContentRoot) {
this.gitHubNotebooksContentRoot.children = [];
pinnedRepos?.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
const repoTreeItem: NotebookContentItem = {
name: repoFullName,
path: ResourceTreeAdapter.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.gitHubNotebooksContentRoot.children.push(repoTreeItem);
});
this.triggerRender();
}
}
private buildDataTree(): TreeNode { private buildDataTree(): TreeNode {
const databaseTreeNodes: TreeNode[] = this.container.nonSystemDatabases().map((database: ViewModels.Database) => { const databaseTreeNodes: TreeNode[] = this.container.nonSystemDatabases().map((database: ViewModels.Database) => {
const databaseNode: TreeNode = { const databaseNode: TreeNode = {
@@ -288,7 +203,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
children.push(schemaNode); children.push(schemaNode);
} }
if (ResourceTreeAdapter.showScriptNodes(this.container)) { if (ResourceTree.showScriptNodes(this.container)) {
children.push(this.buildStoredProcedureNode(collection)); children.push(this.buildStoredProcedureNode(collection));
children.push(this.buildUserDefinedFunctionsNode(collection)); children.push(this.buildUserDefinedFunctionsNode(collection));
children.push(this.buildTriggerNode(collection)); children.push(this.buildTriggerNode(collection));
@@ -329,7 +244,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
); );
}, },
onExpanded: () => { onExpanded: () => {
if (ResourceTreeAdapter.showScriptNodes(this.container)) { if (ResourceTree.showScriptNodes(this.container)) {
collection.loadStoredProcedures(); collection.loadStoredProcedures();
collection.loadUserDefinedFunctions(); collection.loadUserDefinedFunctions();
collection.loadTriggers(); collection.loadTriggers();
@@ -408,7 +323,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
} }
public buildSchemaNode(collection: ViewModels.Collection): TreeNode { public buildSchemaNode(collection: ViewModels.Collection): TreeNode {
if (collection.analyticalStorageTtl() == undefined) { if (collection.analyticalStorageTtl() === undefined) {
return undefined; return undefined;
} }
@@ -429,12 +344,14 @@ export class ResourceTreeAdapter implements ReactAdapter {
} }
private getSchemaNodes(fields: DataModels.IDataField[]): TreeNode[] { private getSchemaNodes(fields: DataModels.IDataField[]): TreeNode[] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const schema: any = {}; const schema: any = {};
//unflatten //unflatten
fields.forEach((field: DataModels.IDataField, fieldIndex: number) => { fields.forEach((field: DataModels.IDataField) => {
const path: string[] = field.path.split("."); const path: string[] = field.path.split(".");
const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`]; const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current: any = {}; let current: any = {};
path.forEach((name: string, pathIndex: number) => { path.forEach((name: string, pathIndex: number) => {
if (pathIndex === 0) { if (pathIndex === 0) {
@@ -459,9 +376,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
}); });
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const traverse = (obj: any): TreeNode[] => { const traverse = (obj: any): TreeNode[] => {
const children: TreeNode[] = []; const children: TreeNode[] = [];
// eslint-disable-next-line no-null/no-null
if (obj !== null && !Array.isArray(obj) && typeof obj === "object") { if (obj !== null && !Array.isArray(obj) && typeof obj === "object") {
Object.entries(obj).forEach(([key, value]) => { Object.entries(obj).forEach(([key, value]) => {
children.push({ label: key, children: traverse(value) }); children.push({ label: key, children: traverse(value) });
@@ -477,21 +396,21 @@ export class ResourceTreeAdapter implements ReactAdapter {
} }
private buildNotebooksTrees(): TreeNode { private buildNotebooksTrees(): TreeNode {
let notebooksTree: TreeNode = { const notebooksTree: TreeNode = {
label: undefined, label: undefined,
isExpanded: true, isExpanded: true,
children: [], children: [],
}; };
if (this.galleryContentRoot) { if (this.props.galleryContentRoot) {
notebooksTree.children.push(this.buildGalleryNotebooksTree()); notebooksTree.children.push(this.buildGalleryNotebooksTree());
} }
if (this.myNotebooksContentRoot) { if (this.props.myNotebooksContentRoot) {
notebooksTree.children.push(this.buildMyNotebooksTree()); notebooksTree.children.push(this.buildMyNotebooksTree());
} }
if (this.gitHubNotebooksContentRoot) { if (this.props.gitHubNotebooksContentRoot) {
// collapse all other notebook nodes // collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false)); notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(this.buildGitHubNotebooksTree()); notebooksTree.children.push(this.buildGitHubNotebooksTree());
@@ -561,7 +480,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
private buildMyNotebooksTree(): TreeNode { private buildMyNotebooksTree(): TreeNode {
const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.myNotebooksContentRoot, this.props.myNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => { this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (hasOpened) {
@@ -582,7 +501,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
private buildGitHubNotebooksTree(): TreeNode { private buildGitHubNotebooksTree(): TreeNode {
const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.gitHubNotebooksContentRoot, this.props.gitHubNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => { this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (hasOpened) {
@@ -654,6 +573,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. /* 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. 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 (activeTab as any).notebookPath() === item.path
); );
}, },
@@ -667,7 +587,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
{ {
label: "Rename", label: "Rename",
iconSrc: NotebookIcon, iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item), onClick: () => this.container.renameNotebook(item).then(() => this.triggerRender()),
}, },
{ {
label: "Delete", label: "Delete",
@@ -756,7 +676,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
{ {
label: "New Directory", label: "New Directory",
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => this.container.onCreateDirectory(item), onClick: () => this.container.onCreateDirectory(item).then(() => this.triggerRender()),
}, },
{ {
label: "New Notebook", label: "New Notebook",
@@ -809,20 +729,19 @@ export class ResourceTreeAdapter implements ReactAdapter {
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab. /* 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. 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 (activeTab as any).notebookPath() === item.path
); );
}, },
contextMenu: contextMenu:
createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath createDirectoryContextMenu && item.path !== PseudoDirPath ? this.createDirectoryContextMenu(item) : undefined,
? this.createDirectoryContextMenu(item)
: undefined,
data: item, data: item,
children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu), children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu),
}; };
} }
public triggerRender() { private triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now())); this.setState({});
} }
/** /**

View File

@@ -1,6 +1,6 @@
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import * as ko from "knockout"; import * as ko from "knockout";
import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; import { ResourceTree } from "./ResourceTree";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
@@ -26,22 +26,40 @@ describe("ResourceTreeAdapter", () => {
it("it should not select if no selected node", () => { it("it should not select if no selected node", () => {
const explorer = mockContainer(); const explorer = mockContainer();
explorer.selectedNode(undefined); explorer.selectedNode(undefined);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const resourceTree = new ResourceTree({
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined); explorer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();
}); });
it("it should not select incorrect subnodekinds", () => { it("it should not select incorrect subnodekinds", () => {
const resourceTreeAdapter = new ResourceTreeAdapter(mockContainer()); const resourceTree = new ResourceTree({
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined); explorer: mockContainer(),
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();
}); });
it("it should not select if no active tab", () => { it("it should not select if no active tab", () => {
const explorer = mockContainer(); const explorer = mockContainer();
explorer.tabsManager.activeTab(undefined); explorer.tabsManager.activeTab(undefined);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const resourceTree = new ResourceTree({
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined); explorer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();
}); });
@@ -54,8 +72,14 @@ describe("ResourceTreeAdapter", () => {
id: ko.observable<string>("dbid"), id: ko.observable<string>("dbid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind), selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as unknown) as ViewModels.TreeNode); } as unknown) as ViewModels.TreeNode);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const resourceTree = new ResourceTree({
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", undefined, [ explorer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("dbid", undefined, [
ViewModels.CollectionTabKind.Documents, ViewModels.CollectionTabKind.Documents,
]); ]);
expect(isDataNodeSelected).toBeTruthy(); expect(isDataNodeSelected).toBeTruthy();
@@ -74,8 +98,14 @@ describe("ResourceTreeAdapter", () => {
id: ko.observable<string>("collid"), id: ko.observable<string>("collid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind), selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as unknown) as ViewModels.TreeNode); } as unknown) as ViewModels.TreeNode);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const resourceTree = new ResourceTree({
let isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]); explorer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
let isDataNodeSelected = resourceTree.isDataNodeSelected("dbid", "collid", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy(); expect(isDataNodeSelected).toBeTruthy();
subNodeKind = ViewModels.CollectionTabKind.Graph; subNodeKind = ViewModels.CollectionTabKind.Graph;
@@ -89,7 +119,7 @@ describe("ResourceTreeAdapter", () => {
id: ko.observable<string>("collid"), id: ko.observable<string>("collid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind), selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as unknown) as ViewModels.TreeNode); } as unknown) as ViewModels.TreeNode);
isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]); isDataNodeSelected = resourceTree.isDataNodeSelected("dbid", "collid", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy(); expect(isDataNodeSelected).toBeTruthy();
}); });
@@ -105,8 +135,14 @@ describe("ResourceTreeAdapter", () => {
explorer.tabsManager.activeTab({ explorer.tabsManager.activeTab({
tabKind: ViewModels.CollectionTabKind.Documents, tabKind: ViewModels.CollectionTabKind.Documents,
} as TabsBase); } as TabsBase);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer); const resourceTree = new ResourceTree({
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [ explorer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("dbid", "collid", [
ViewModels.CollectionTabKind.Settings, ViewModels.CollectionTabKind.Settings,
]); ]);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();

View File

@@ -2,7 +2,7 @@ import * as ko from "knockout";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import React from "react"; import React from "react";
import { ResourceTreeAdapter } from "./ResourceTreeAdapter"; import { ResourceTree } from "./ResourceTree";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@@ -237,7 +237,13 @@ const createMockCollection = (): ViewModels.Collection => {
describe("Resource tree for schema", () => { describe("Resource tree for schema", () => {
const mockContainer: Explorer = createMockContainer(); const mockContainer: Explorer = createMockContainer();
const resourceTree = new ResourceTreeAdapter(mockContainer); const resourceTree = new ResourceTree({
explorer: mockContainer,
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
it("should render", () => { it("should render", () => {
const rootNode: TreeNode = resourceTree.buildSchemaNode(createMockCollection()); const rootNode: TreeNode = resourceTree.buildSchemaNode(createMockCollection());

View File

@@ -45,7 +45,7 @@ import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less"; import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less"; import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less"; import "./Explorer/Controls/TreeComponent/treeComponent.less";
import { ExplorerParams } from "./Explorer/Explorer"; import Explorer, { ExplorerParams } from "./Explorer/Explorer";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less"; import "./Explorer/Graph/NewVertexComponent/newVertexComponent.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
@@ -60,6 +60,7 @@ import "./Explorer/SplashScreen/SplashScreen.less";
import "./Explorer/Tabs/QueryTab.less"; import "./Explorer/Tabs/QueryTab.less";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useNotebooks } from "./hooks/useNotebooks";
import { useSidePanel } from "./hooks/useSidePanel"; import { useSidePanel } from "./hooks/useSidePanel";
import { KOCommentEnd, KOCommentIfStart } from "./koComment"; import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import "./Libs/is-integer-polyfill"; import "./Libs/is-integer-polyfill";
@@ -87,6 +88,18 @@ const App: React.FunctionComponent = () => {
const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel(); const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel();
// 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 = { const explorerParams: ExplorerParams = {
setIsNotificationConsoleExpanded, setIsNotificationConsoleExpanded,
setNotificationConsoleData, setNotificationConsoleData,
@@ -95,10 +108,14 @@ const App: React.FunctionComponent = () => {
closeSidePanel, closeSidePanel,
openDialog, openDialog,
closeDialog, closeDialog,
onRefreshNotebookList: refreshList,
initializeGitHubRepos,
getMyNotebooksContentRoot,
}; };
const config = useConfig(); const config = useConfig();
const explorer = useKnockoutExplorer(config?.platform, explorerParams); const explorer = useKnockoutExplorer(config?.platform, explorerParams);
context.container = explorer;
if (!explorer) { if (!explorer) {
return <LoadingExplorer />; return <LoadingExplorer />;
} }
@@ -158,7 +175,15 @@ const App: React.FunctionComponent = () => {
style={{ overflowY: "auto" }} style={{ overflowY: "auto" }}
data-bind="if: isAuthWithResourceToken(), react:resourceTreeForResourceToken" data-bind="if: isAuthWithResourceToken(), react:resourceTreeForResourceToken"
/> />
<div style={{ overflowY: "auto" }} data-bind="if: !isAuthWithResourceToken(), react:resourceTree" /> <div style={{ overflowY: "auto" }} data-bind="if: !isAuthWithResourceToken()">
<ResourceTree
explorer={explorer}
lastRefreshedTime={lastRefreshTime}
galleryContentRoot={galleryContentRoot}
myNotebooksContentRoot={myNotebooksContentRoot}
gitHubNotebooksContentRoot={gitHubNotebooksContentRoot}
/>
</div>
</div> </div>
{/* Collections Window - End */} {/* Collections Window - End */}
</div> </div>

149
src/hooks/useNotebooks.ts Normal file
View File

@@ -0,0 +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 = (context: { container: Explorer }): NotebookHooks => {
const [lastRefreshTime, setLastRefreshTime] = useState<number>(undefined);
const [galleryContentRoot, setGalleryContentRoot] = useState<NotebookContentItem>(undefined);
const [myNotebooksContentRoot, setMyNotebooksContentRoot] = useState<NotebookContentItem>(undefined);
const [gitHubNotebooksContentRoot, setGitHubNotebooksContentRoot] = useState<NotebookContentItem>(undefined);
const refreshList = (): void => {
initialize();
setLastRefreshTime(new Date().getTime());
};
// TODO For now, we need to rely on this, as setMyNotebooksContentRoot() is not synchronous
let _myNotebooksContentRoot: NotebookContentItem;
const _setMyNotebooksContentRoot = (newValue: NotebookContentItem) => {
_myNotebooksContentRoot = newValue;
setMyNotebooksContentRoot(newValue);
};
const initialize = (): Promise<void[]> => {
const refreshTasks: Promise<void>[] = [];
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;
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,
};
};