mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-24 04:04:13 +00:00
Compare commits
1 Commits
languy-res
...
languy-com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8141799ffd |
4
config.json
Normal file
4
config.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"GITHUB_CLIENT_ID": "167ea4b09801db1de03d",
|
||||||
|
"GITHUB_CLIENT_SECRET": "e7bb10a3a8da428815805c6fc483560a035a73c1"
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
.main {
|
.main {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
border-right: 1px solid @BaseMedium;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resourceTreeScroll {
|
.resourceTreeScroll {
|
||||||
|
|||||||
26359
package-lock.json
generated
26359
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
107
src/Explorer/Menus/CommandBar/CommandBarComponent.tsx
Normal file
107
src/Explorer/Menus/CommandBar/CommandBarComponent.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
35
src/Main.tsx
35
src/Main.tsx
@@ -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>
|
||||||
|
|||||||
15
src/hooks/useExplorerState.ts
Normal file
15
src/hooks/useExplorerState.ts
Normal 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 {};
|
||||||
|
};
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user