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 {
height: 100%;
}
border-right: 1px solid @BaseMedium;
}
.resourceTreeScroll {

View File

@@ -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 {

View File

@@ -986,6 +986,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function],
"openDialog": undefined,
"openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -1018,26 +1019,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [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 {
"container": [Circular],
"parameters": [Function],
@@ -2187,6 +2168,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function],
"openDialog": undefined,
"openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -2219,26 +2201,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [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 {
"container": [Circular],
"parameters": [Function],
@@ -3401,6 +3363,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function],
"openDialog": undefined,
"openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -3433,26 +3396,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [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 {
"container": [Circular],
"parameters": [Function],
@@ -4602,6 +4545,7 @@ exports[`SettingsComponent renders 1`] = `
"onSwitchToConnectionString": [Function],
"openDialog": undefined,
"openSidePanel": undefined,
"params": undefined,
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -4634,26 +4578,6 @@ exports[`SettingsComponent renders 1`] = `
"resourceTokenCollectionId": [Function],
"resourceTokenDatabaseId": [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 {
"container": [Circular],
"parameters": [Function],

View File

@@ -21,7 +21,7 @@ import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import * as ViewModels from "../Contracts/ViewModels";
import { IGalleryItem } from "../Juno/JunoClient";
import { IGalleryItem, IPinnedRepo } from "../Juno/JunoClient";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { RouteHandler } from "../RouteHandlers/RouteHandler";
@@ -77,7 +77,6 @@ import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
@@ -95,6 +94,10 @@ export interface ExplorerParams {
closeSidePanel: () => void;
closeDialog: () => void;
openDialog: (props: DialogProps) => void;
onRefreshNotebookList: () => void;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
getMyNotebooksContentRoot: () => NotebookContentItem;
}
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.
* */
public isRefreshingExplorer: ko.Observable<boolean>;
private resourceTree: ResourceTreeAdapter;
// Resource Token
public resourceTokenDatabaseId: ko.Observable<string>;
@@ -278,7 +280,7 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor(params?: ExplorerParams) {
constructor(public params?: ExplorerParams) {
this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded;
this.setNotificationConsoleData = params?.setNotificationConsoleData;
this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted;
@@ -878,7 +880,6 @@ export default class Explorer {
this.notebookManager.initialize({
container: this,
notebookBasePath: this.notebookBasePath,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
@@ -893,7 +894,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<DataModels.NotebookWorkspaceConnectionInfo>({
notebookServerEndpoint: undefined,
@@ -1707,7 +1707,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;
}
@@ -1715,7 +1715,7 @@ export default class Explorer {
public async importAndOpen(path: string): Promise<boolean> {
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);
@@ -1732,7 +1732,8 @@ export default class Explorer {
}
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 (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) {
@@ -1917,7 +1918,6 @@ export default class Explorer {
return newNotebookFile;
});
result.then(() => this.resourceTree.triggerRender());
return result;
}
@@ -1938,7 +1938,6 @@ export default class Explorer {
defaultInput: "",
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input),
});
result.then(() => this.resourceTree.triggerRender());
return result;
}
@@ -2094,12 +2093,14 @@ export default class Explorer {
return false;
}
};
private refreshNotebookList = async (): Promise<void> => {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
return;
}
await this.resourceTree.initialize();
this.params?.onRefreshNotebookList();
this.notebookManager?.refreshPinnedRepos();
if (this.notebookToImport) {
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
@@ -2162,7 +2163,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,
@@ -2186,7 +2187,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);
@@ -2204,7 +2205,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",
@@ -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) {
const error = "Attempt to refresh notebook list, but notebook is not enabled";
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
* @param item
* @return updated item
*/
public updateItemChildren(item: NotebookContentItem): Promise<void> {
public updateItemChildren(item: NotebookContentItem): Promise<NotebookContentItem> {
return this.fetchNotebookFiles(item.path).then((subItems) => {
item.children = subItems;
subItems.forEach((subItem) => (subItem.parent = item));
return item;
});
}

View File

@@ -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,6 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export interface NotebookManagerOptions {
container: Explorer;
notebookBasePath: ko.Observable<string>;
resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void;
refreshNotebookList: () => void;
}
@@ -107,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();
}

View File

@@ -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,

View File

@@ -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<CopyNotebookPaneP
options.push({
key: "MyNotebooks-Item",
text: ResourceTreeAdapter.MyNotebooksTitle,
title: ResourceTreeAdapter.MyNotebooksTitle,
text: Notebook.MyNotebooksTitle,
title: Notebook.MyNotebooksTitle,
data: {
type: "MyNotebooks",
} as Location,
@@ -86,7 +86,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
options.push({
key: "GitHub-Header",
text: ResourceTreeAdapter.GitHubReposTitle,
text: Notebook.GitHubReposTitle,
itemType: SelectableOptionMenuItemType.Header,
});

View File

@@ -1,8 +1,7 @@
import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
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 { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
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 { ArrayHashMap } from "../../Common/ArrayHashMap";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import _ from "underscore";
import { IPinnedRepo } from "../../Juno/JunoClient";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../Common/Constants";
@@ -34,31 +31,39 @@ 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 class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks";
public static readonly GitHubReposTitle = "GitHub repos";
export interface ResourceTreeProps {
// TODO remove eventually
explorer: Explorer;
private static readonly DataTitle = "DATA";
private static readonly NotebooksTitle = "NOTEBOOKS";
private static readonly PseudoDirPath = "PsuedoDir";
lastRefreshedTime: number;
public parameters: ko.Observable<number>;
public galleryContentRoot: NotebookContentItem;
public myNotebooksContentRoot: NotebookContentItem;
public gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem;
myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem;
}
export class ResourceTree extends React.Component<ResourceTreeProps> {
private koSubsDatabaseIdMap: ArrayHashMap<ko.Subscription>; // database id -> ko subs
private koSubsCollectionIdMap: ArrayHashMap<ko.Subscription>; // collection id -> ko subs
private databaseCollectionIdMap: ArrayHashMap<string>; // 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();
@@ -73,34 +78,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
});
this.container.nonSystemDatabases().forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.triggerRender();
}
private traceMyNotebookTreeInfo() {
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 {
render(): JSX.Element {
const dataRootNode = this.buildDataTree();
const notebooksRootNode = this.buildNotebooksTrees();
@@ -108,15 +88,15 @@ export class ResourceTreeAdapter implements ReactAdapter {
return (
<>
<AccordionComponent>
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
<AccordionItemComponent title={DataTitle} isExpanded={!this.props.gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}>
<AccordionItemComponent title={NotebooksTitle}>
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
</AccordionItemComponent>
</AccordionComponent>
{this.galleryContentRoot && this.buildGalleryCallout()}
{this.props.galleryContentRoot && this.buildGalleryCallout()}
</>
);
} 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 {
const databaseTreeNodes: TreeNode[] = this.container.nonSystemDatabases().map((database: ViewModels.Database) => {
const databaseNode: TreeNode = {
@@ -288,7 +203,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));
@@ -329,7 +244,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
);
},
onExpanded: () => {
if (ResourceTreeAdapter.showScriptNodes(this.container)) {
if (ResourceTree.showScriptNodes(this.container)) {
collection.loadStoredProcedures();
collection.loadUserDefinedFunctions();
collection.loadTriggers();
@@ -408,7 +323,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
}
public buildSchemaNode(collection: ViewModels.Collection): TreeNode {
if (collection.analyticalStorageTtl() == undefined) {
if (collection.analyticalStorageTtl() === undefined) {
return undefined;
}
@@ -429,12 +344,14 @@ 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}`];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current: any = {};
path.forEach((name: string, pathIndex: number) => {
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 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) });
@@ -477,21 +396,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.props.galleryContentRoot) {
notebooksTree.children.push(this.buildGalleryNotebooksTree());
}
if (this.myNotebooksContentRoot) {
if (this.props.myNotebooksContentRoot) {
notebooksTree.children.push(this.buildMyNotebooksTree());
}
if (this.gitHubNotebooksContentRoot) {
if (this.props.gitHubNotebooksContentRoot) {
// collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(this.buildGitHubNotebooksTree());
@@ -561,7 +480,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
private buildMyNotebooksTree(): TreeNode {
const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.myNotebooksContentRoot,
this.props.myNotebooksContentRoot,
(item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) {
@@ -582,7 +501,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
private buildGitHubNotebooksTree(): TreeNode {
const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.gitHubNotebooksContentRoot,
this.props.gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
this.container.openNotebook(item).then((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.
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
);
},
@@ -667,7 +587,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item),
onClick: () => this.container.renameNotebook(item).then(() => this.triggerRender()),
},
{
label: "Delete",
@@ -756,7 +676,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
{
label: "New Directory",
iconSrc: NewNotebookIcon,
onClick: () => this.container.onCreateDirectory(item),
onClick: () => this.container.onCreateDirectory(item).then(() => this.triggerRender()),
},
{
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.
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
? this.createDirectoryContextMenu(item)
: undefined,
createDirectoryContextMenu && item.path !== PseudoDirPath ? this.createDirectoryContextMenu(item) : undefined,
data: item,
children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu),
};
}
public triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now()));
private triggerRender() {
this.setState({});
}
/**

View File

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

View File

@@ -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 { ResourceTree } from "./ResourceTree";
import { shallow } from "enzyme";
import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
@@ -237,7 +237,13 @@ const createMockCollection = (): ViewModels.Collection => {
describe("Resource tree for schema", () => {
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", () => {
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/Notebook/NotebookTerminalComponent.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/NewVertexComponent/newVertexComponent.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
@@ -60,6 +60,7 @@ import "./Explorer/SplashScreen/SplashScreen.less";
import "./Explorer/Tabs/QueryTab.less";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useNotebooks } from "./hooks/useNotebooks";
import { useSidePanel } from "./hooks/useSidePanel";
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import "./Libs/is-integer-polyfill";
@@ -87,6 +88,18 @@ const App: React.FunctionComponent = () => {
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 = {
setIsNotificationConsoleExpanded,
setNotificationConsoleData,
@@ -95,10 +108,14 @@ const App: React.FunctionComponent = () => {
closeSidePanel,
openDialog,
closeDialog,
onRefreshNotebookList: refreshList,
initializeGitHubRepos,
getMyNotebooksContentRoot,
};
const config = useConfig();
const explorer = useKnockoutExplorer(config?.platform, explorerParams);
context.container = explorer;
if (!explorer) {
return <LoadingExplorer />;
}
@@ -158,7 +175,15 @@ const App: React.FunctionComponent = () => {
style={{ overflowY: "auto" }}
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>
{/* Collections Window - End */}
</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,
};
};