Compare commits

..

1 Commits

Author SHA1 Message Date
Laurent Nguyen
8141799ffd Initial move of command bar to react 2021-04-14 11:44:22 +02:00
18 changed files with 410 additions and 26770 deletions

4
config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"GITHUB_CLIENT_ID": "167ea4b09801db1de03d",
"GITHUB_CLIENT_SECRET": "e7bb10a3a8da428815805c6fc483560a035a73c1"
}

View File

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

26359
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -392,9 +392,6 @@ 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,7 +986,6 @@ 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],
@@ -1019,6 +1018,26 @@ 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],
@@ -2168,7 +2187,6 @@ 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],
@@ -2201,6 +2219,26 @@ 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],
@@ -3363,7 +3401,6 @@ 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],
@@ -3396,6 +3433,26 @@ 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],
@@ -4545,7 +4602,6 @@ 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],
@@ -4578,6 +4634,26 @@ 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, IPinnedRepo } from "../Juno/JunoClient"; import { IGalleryItem } 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,6 +77,7 @@ 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";
@@ -94,10 +95,6 @@ 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 {
@@ -194,6 +191,7 @@ 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>;
@@ -280,7 +278,7 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor(public params?: ExplorerParams) { constructor(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;
@@ -880,6 +878,7 @@ 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(),
}); });
@@ -894,6 +893,7 @@ 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.params.onRefreshNotebookList()) .then(() => this.resourceTree.triggerRender())
.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.params.getMyNotebooksContentRoot(); const parent = this.resourceTree.myNotebooksContentRoot;
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,8 +1732,7 @@ 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.params.getMyNotebooksContentRoot(); 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) {
if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) {
@@ -1918,6 +1917,7 @@ export default class Explorer {
return newNotebookFile; return newNotebookFile;
}); });
result.then(() => this.resourceTree.triggerRender());
return result; return result;
} }
@@ -1938,6 +1938,7 @@ 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;
} }
@@ -2093,14 +2094,12 @@ 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;
} }
this.params?.onRefreshNotebookList(); await this.resourceTree.initialize();
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);
@@ -2163,7 +2162,7 @@ export default class Explorer {
throw new Error(error); throw new Error(error);
} }
parent = parent || this.params.getMyNotebooksContentRoot(); parent = parent || this.resourceTree.myNotebooksContentRoot;
const notificationProgressId = NotificationConsoleUtils.logConsoleMessage( const notificationProgressId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
@@ -2187,7 +2186,7 @@ export default class Explorer {
); );
return this.openNotebook(newFile); return this.openNotebook(newFile);
}) })
.then(() => this.params.onRefreshNotebookList()) .then(() => this.resourceTree.triggerRender())
.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);
@@ -2205,7 +2204,7 @@ export default class Explorer {
} }
public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void { public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void {
parent = parent || this.params.getMyNotebooksContentRoot(); parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePane.openWithOptions({ this.uploadFilePane.openWithOptions({
paneTitle: "Upload file to notebook server", paneTitle: "Upload file to notebook server",
@@ -2236,7 +2235,7 @@ export default class Explorer {
}); });
} }
public refreshContentItem(item: NotebookContentItem): Promise<NotebookContentItem> { public refreshContentItem(item: NotebookContentItem): Promise<void> {
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

@@ -0,0 +1,107 @@
/**
* This adapter is responsible to render the React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as ko from "knockout";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import * as React from "react";
import { StyleConstants } from "../../../Common/Constants";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
export interface CommandBarComponentProps {
isNotebookTabActive: boolean;
tabsButtons: CommandButtonComponentProps[];
}
export const CommandBarComponent: React.FunctionComponent = ({ isNotebookTabActive, tabsButtons }: CommandBarComponentProps) {
constructor(props: CommandBarComponentProps) {
super(props);
this.state = {
isNotebookTabActive: false
}
this.container = container;
this.tabsButtons = [];
// this.isNotebookTabActive = ko.computed(() =>
// container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2)
// );
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
const toWatch = [
container.isPreferredApiTable,
container.isPreferredApiMongoDB,
container.isPreferredApiDocumentDB,
container.isPreferredApiCassandra,
container.isPreferredApiGraph,
container.deleteCollectionText,
container.deleteDatabaseText,
container.addCollectionText,
container.addDatabaseText,
container.isDatabaseNodeOrNoneSelected,
container.isDatabaseNodeSelected,
container.isNoneSelected,
container.isResourceTokenCollectionNodeSelected,
container.isHostedDataExplorerEnabled,
container.isSynapseLinkUpdating,
container.databaseAccount,
this.isNotebookTabActive,
container.isServerlessEnabled,
];
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
this.parameters = ko.observable(Date.now());
}
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
this.tabsButtons = buttons;
this.triggerRender();
}
const backgroundColor = StyleConstants.BaseLight;
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(this.container);
const contextButtons = (this.tabsButtons || []).concat(
CommandBarComponentButtonFactory.createContextCommandBarButtons(this.container)
);
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(this.container);
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
if (this.tabsButtons && this.tabsButtons.length > 0) {
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
}
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
if (uiFabricTabsButtons.length > 0) {
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
}
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (props.isNotebookTabActive) {
uiFabricControlButtons.unshift(
CommandBarUtil.createMemoryTracker("memoryTracker", this.container.memoryUsageInfo)
);
}
return (
<React.Fragment>
<div className="commandBarContainer">
<CommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
farItems={uiFabricControlButtons}
styles={{
root: { backgroundColor: backgroundColor },
}}
overflowButtonProps={{ ariaLabel: "More commands" }}
/>
</div>
</React.Fragment>
);
}

View File

@@ -1,110 +0,0 @@
/**
* This adapter is responsible to render the React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { StyleConstants } from "../../../Common/Constants";
import * as CommandBarUtil from "./CommandBarUtil";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class CommandBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
public container: Explorer;
private tabsButtons: CommandButtonComponentProps[];
private isNotebookTabActive: ko.Computed<boolean>;
constructor(container: Explorer) {
this.container = container;
this.tabsButtons = [];
this.isNotebookTabActive = ko.computed(() =>
container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2)
);
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
const toWatch = [
container.isPreferredApiTable,
container.isPreferredApiMongoDB,
container.isPreferredApiDocumentDB,
container.isPreferredApiCassandra,
container.isPreferredApiGraph,
container.deleteCollectionText,
container.deleteDatabaseText,
container.addCollectionText,
container.addDatabaseText,
container.isDatabaseNodeOrNoneSelected,
container.isDatabaseNodeSelected,
container.isNoneSelected,
container.isResourceTokenCollectionNodeSelected,
container.isHostedDataExplorerEnabled,
container.isSynapseLinkUpdating,
container.databaseAccount,
this.isNotebookTabActive,
container.isServerlessEnabled,
];
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
this.parameters = ko.observable(Date.now());
}
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
this.tabsButtons = buttons;
this.triggerRender();
}
public renderComponent(): JSX.Element {
const backgroundColor = StyleConstants.BaseLight;
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(this.container);
const contextButtons = (this.tabsButtons || []).concat(
CommandBarComponentButtonFactory.createContextCommandBarButtons(this.container)
);
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(this.container);
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
if (this.tabsButtons && this.tabsButtons.length > 0) {
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
}
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
if (uiFabricTabsButtons.length > 0) {
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
}
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (this.isNotebookTabActive()) {
uiFabricControlButtons.unshift(
CommandBarUtil.createMemoryTracker("memoryTracker", this.container.memoryUsageInfo)
);
}
return (
<React.Fragment>
<div className="commandBarContainer">
<CommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
farItems={uiFabricControlButtons}
styles={{
root: { backgroundColor: backgroundColor },
}}
overflowButtonProps={{ ariaLabel: "More commands" }}
/>
</div>
</React.Fragment>
);
}
private triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@@ -18,13 +18,11 @@ 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<NotebookContentItem> { public updateItemChildren(item: NotebookContentItem): Promise<void> {
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,6 +18,7 @@ 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";
@@ -29,6 +30,7 @@ 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;
} }
@@ -105,8 +107,8 @@ export default class NotebookManager {
}); });
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
// TODO Move this out of NotebookManager? this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
this.params.container.params.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.triggerRender();
}); });
this.refreshPinnedRepos(); this.refreshPinnedRepos();
} }

View File

@@ -8,9 +8,10 @@ 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, Notebook } from "../../Common/Constants"; import { HttpStatusCodes } 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 {
@@ -150,7 +151,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
switch (location.type) { switch (location.type) {
case "MyNotebooks": case "MyNotebooks":
parent = { parent = {
name: Notebook.MyNotebooksTitle, name: ResourceTreeAdapter.MyNotebooksTitle,
path: this.container.getNotebookBasePath(), path: this.container.getNotebookBasePath(),
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
@@ -158,7 +159,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
case "GitHub": case "GitHub":
parent = { parent = {
name: Notebook.GitHubReposTitle, name: ResourceTreeAdapter.GitHubReposTitle,
path: GitHubUtils.toContentUri( path: GitHubUtils.toContentUri(
this.selectedLocation.owner, this.selectedLocation.owner,
this.selectedLocation.repo, this.selectedLocation.repo,

View File

@@ -1,6 +1,7 @@
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,
@@ -12,7 +13,6 @@ 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: Notebook.MyNotebooksTitle, text: ResourceTreeAdapter.MyNotebooksTitle,
title: Notebook.MyNotebooksTitle, title: ResourceTreeAdapter.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: Notebook.GitHubReposTitle, text: ResourceTreeAdapter.GitHubReposTitle,
itemType: SelectableOptionMenuItemType.Header, itemType: SelectableOptionMenuItemType.Header,
}); });

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 { ResourceTree } from "./ResourceTree"; import { ResourceTreeAdapter } from "./ResourceTreeAdapter";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
@@ -26,40 +26,22 @@ 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 resourceTree = new ResourceTree({ const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
explorer, const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined);
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 resourceTree = new ResourceTree({ const resourceTreeAdapter = new ResourceTreeAdapter(mockContainer());
explorer: mockContainer(), const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined);
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 resourceTree = new ResourceTree({ const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
explorer, const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined);
lastRefreshedTime: 0,
galleryContentRoot: undefined,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
});
const isDataNodeSelected = resourceTree.isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy(); expect(isDataNodeSelected).toBeFalsy();
}); });
@@ -72,14 +54,8 @@ 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 resourceTree = new ResourceTree({ const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
explorer, const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", undefined, [
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();
@@ -98,14 +74,8 @@ 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 resourceTree = new ResourceTree({ const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
explorer, let isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]);
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;
@@ -119,7 +89,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 = resourceTree.isDataNodeSelected("dbid", "collid", [subNodeKind]); isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy(); expect(isDataNodeSelected).toBeTruthy();
}); });
@@ -135,14 +105,8 @@ describe("ResourceTreeAdapter", () => {
explorer.tabsManager.activeTab({ explorer.tabsManager.activeTab({
tabKind: ViewModels.CollectionTabKind.Documents, tabKind: ViewModels.CollectionTabKind.Documents,
} as TabsBase); } as TabsBase);
const resourceTree = new ResourceTree({ const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
explorer, const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [
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 { ResourceTree } from "./ResourceTree"; import { ResourceTreeAdapter } from "./ResourceTreeAdapter";
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,13 +237,7 @@ 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 ResourceTree({ const resourceTree = new ResourceTreeAdapter(mockContainer);
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

@@ -1,7 +1,8 @@
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 } from "../Controls/TreeComponent/TreeComponent"; import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } 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";
@@ -17,6 +18,8 @@ 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";
@@ -31,39 +34,31 @@ 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 interface ResourceTreeProps { export class ResourceTreeAdapter implements ReactAdapter {
// TODO remove eventually public static readonly MyNotebooksTitle = "My Notebooks";
explorer: Explorer; public static readonly GitHubReposTitle = "GitHub repos";
lastRefreshedTime: number; private static readonly DataTitle = "DATA";
private static readonly NotebooksTitle = "NOTEBOOKS";
private static readonly PseudoDirPath = "PsuedoDir";
galleryContentRoot: NotebookContentItem; public parameters: ko.Observable<number>;
myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem; public galleryContentRoot: 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
private readonly container: Explorer; public constructor(private container: Explorer) {
this.parameters = ko.observable(Date.now());
constructor(props: ResourceTreeProps) { this.container.selectedNode.subscribe((newValue: any) => this.triggerRender());
super(props); this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender());
this.state = { this.container.isNotebookEnabled.subscribe((newValue) => this.triggerRender());
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();
@@ -78,9 +73,34 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
}); });
this.container.nonSystemDatabases().forEach((database: ViewModels.Database) => this.watchDatabase(database)); this.container.nonSystemDatabases().forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.triggerRender();
} }
render(): JSX.Element { 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 {
const dataRootNode = this.buildDataTree(); const dataRootNode = this.buildDataTree();
const notebooksRootNode = this.buildNotebooksTrees(); const notebooksRootNode = this.buildNotebooksTrees();
@@ -88,15 +108,15 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
return ( return (
<> <>
<AccordionComponent> <AccordionComponent>
<AccordionItemComponent title={DataTitle} isExpanded={!this.props.gitHubNotebooksContentRoot}> <AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} /> <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent> </AccordionItemComponent>
<AccordionItemComponent title={NotebooksTitle}> <AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}>
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} /> <TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
</AccordionItemComponent> </AccordionItemComponent>
</AccordionComponent> </AccordionComponent>
{this.props.galleryContentRoot && this.buildGalleryCallout()} {this.galleryContentRoot && this.buildGalleryCallout()}
</> </>
); );
} else { } else {
@@ -104,6 +124,71 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
} }
} }
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 = {
@@ -203,7 +288,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
children.push(schemaNode); children.push(schemaNode);
} }
if (ResourceTree.showScriptNodes(this.container)) { if (ResourceTreeAdapter.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));
@@ -244,7 +329,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
); );
}, },
onExpanded: () => { onExpanded: () => {
if (ResourceTree.showScriptNodes(this.container)) { if (ResourceTreeAdapter.showScriptNodes(this.container)) {
collection.loadStoredProcedures(); collection.loadStoredProcedures();
collection.loadUserDefinedFunctions(); collection.loadUserDefinedFunctions();
collection.loadTriggers(); collection.loadTriggers();
@@ -323,7 +408,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
} }
public buildSchemaNode(collection: ViewModels.Collection): TreeNode { public buildSchemaNode(collection: ViewModels.Collection): TreeNode {
if (collection.analyticalStorageTtl() === undefined) { if (collection.analyticalStorageTtl() == undefined) {
return undefined; return undefined;
} }
@@ -344,14 +429,12 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
} }
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) => { fields.forEach((field: DataModels.IDataField, fieldIndex: number) => {
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) {
@@ -376,11 +459,9 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
}); });
}); });
// 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) });
@@ -396,21 +477,21 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
} }
private buildNotebooksTrees(): TreeNode { private buildNotebooksTrees(): TreeNode {
const notebooksTree: TreeNode = { let notebooksTree: TreeNode = {
label: undefined, label: undefined,
isExpanded: true, isExpanded: true,
children: [], children: [],
}; };
if (this.props.galleryContentRoot) { if (this.galleryContentRoot) {
notebooksTree.children.push(this.buildGalleryNotebooksTree()); notebooksTree.children.push(this.buildGalleryNotebooksTree());
} }
if (this.props.myNotebooksContentRoot) { if (this.myNotebooksContentRoot) {
notebooksTree.children.push(this.buildMyNotebooksTree()); notebooksTree.children.push(this.buildMyNotebooksTree());
} }
if (this.props.gitHubNotebooksContentRoot) { if (this.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());
@@ -480,7 +561,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
private buildMyNotebooksTree(): TreeNode { private buildMyNotebooksTree(): TreeNode {
const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.props.myNotebooksContentRoot, this.myNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => { this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (hasOpened) {
@@ -501,7 +582,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
private buildGitHubNotebooksTree(): TreeNode { private buildGitHubNotebooksTree(): TreeNode {
const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode( const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.props.gitHubNotebooksContentRoot, this.gitHubNotebooksContentRoot,
(item: NotebookContentItem) => { (item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => { this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) { if (hasOpened) {
@@ -573,7 +654,6 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
/* 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
); );
}, },
@@ -587,7 +667,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
{ {
label: "Rename", label: "Rename",
iconSrc: NotebookIcon, iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item).then(() => this.triggerRender()), onClick: () => this.container.renameNotebook(item),
}, },
{ {
label: "Delete", label: "Delete",
@@ -676,7 +756,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
{ {
label: "New Directory", label: "New Directory",
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
onClick: () => this.container.onCreateDirectory(item).then(() => this.triggerRender()), onClick: () => this.container.onCreateDirectory(item),
}, },
{ {
label: "New Notebook", label: "New Notebook",
@@ -729,19 +809,20 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
/* 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 !== PseudoDirPath ? this.createDirectoryContextMenu(item) : undefined, createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath
? this.createDirectoryContextMenu(item)
: undefined,
data: item, data: item,
children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu), children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu),
}; };
} }
private triggerRender() { public triggerRender() {
this.setState({}); window.requestAnimationFrame(() => this.parameters(Date.now()));
} }
/** /**

View File

@@ -45,9 +45,10 @@ 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 Explorer, { ExplorerParams } from "./Explorer/Explorer"; import { 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 { CommandBarComponent } from "./Explorer/Menus/CommandBar/CommandBarComponent";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
@@ -59,8 +60,8 @@ import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen";
import "./Explorer/SplashScreen/SplashScreen.less"; 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 { useExplorerState } from "./hooks/useExplorerState";
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";
@@ -88,18 +89,6 @@ 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,
@@ -108,14 +97,12 @@ 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; const { commandBarProperties } = useExplorerState(explorer);
if (!explorer) { if (!explorer) {
return <LoadingExplorer />; return <LoadingExplorer />;
} }
@@ -124,7 +111,7 @@ const App: React.FunctionComponent = () => {
<div className="flexContainer"> <div className="flexContainer">
<div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}> <div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
{/* Main Command Bar - Start */} {/* Main Command Bar - Start */}
<div data-bind="react: commandBarComponentAdapter" /> <CommandBarComponent {...commandBarProperties} />
{/* Collections Tree and Tabs - Begin */} {/* Collections Tree and Tabs - Begin */}
<div className="resourceTreeAndTabs"> <div className="resourceTreeAndTabs">
{/* Collections Tree - Start */} {/* Collections Tree - Start */}
@@ -175,15 +162,7 @@ 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()"> <div style={{ overflowY: "auto" }} data-bind="if: !isAuthWithResourceToken(), react:resourceTree" />
<ResourceTree
explorer={explorer}
lastRefreshedTime={lastRefreshTime}
galleryContentRoot={galleryContentRoot}
myNotebooksContentRoot={myNotebooksContentRoot}
gitHubNotebooksContentRoot={gitHubNotebooksContentRoot}
/>
</div>
</div> </div>
{/* Collections Window - End */} {/* Collections Window - End */}
</div> </div>

View File

@@ -0,0 +1,15 @@
import { useState } from "react";
import Explorer from "../Explorer/Explorer";
export interface ExplorerStateProperties {
commandBarProperties: {
}
}
export const useExplorerState = (container: Explorer): ExplorerStateProperties => {
const [isPanelOpen, setIsPanelOpen] = useState<boolean>(false);
return {};
};

View File

@@ -1,149 +0,0 @@
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,
};
};