mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-01-23 17:30:23 +00:00
797 lines
28 KiB
TypeScript
797 lines
28 KiB
TypeScript
import * as ko from "knockout";
|
|
import * as React from "react";
|
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
|
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
|
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
|
import * as ViewModels from "../../Contracts/ViewModels";
|
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
|
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
|
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
|
import { CosmosClient } from "../../Common/CosmosClient";
|
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
|
import DeleteIcon from "../../../images/delete.svg";
|
|
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
|
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
|
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
|
import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
|
import { ArrayHashMap } from "../../Common/ArrayHashMap";
|
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
|
import _ from "underscore";
|
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
|
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
|
import { Areas } from "../../Common/Constants";
|
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
|
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
|
import { Callout, Text, Link, DirectionalHint, Stack, ICalloutProps, ILinkProps } from "office-ui-fabric-react";
|
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
|
import Explorer from "../Explorer";
|
|
import UserDefinedFunction from "./UserDefinedFunction";
|
|
import StoredProcedure from "./StoredProcedure";
|
|
import Trigger from "./Trigger";
|
|
import TabsBase from "../Tabs/TabsBase";
|
|
|
|
export class ResourceTreeAdapter implements ReactAdapter {
|
|
private static readonly DataTitle = "DATA";
|
|
private static readonly NotebooksTitle = "NOTEBOOKS";
|
|
private static readonly PseudoDirPath = "PsuedoDir";
|
|
|
|
public parameters: ko.Observable<number>;
|
|
|
|
public galleryContentRoot: NotebookContentItem;
|
|
public myNotebooksContentRoot: NotebookContentItem;
|
|
public gitHubNotebooksContentRoot: NotebookContentItem;
|
|
|
|
private koSubsDatabaseIdMap: ArrayHashMap<ko.Subscription>; // database id -> ko subs
|
|
private koSubsCollectionIdMap: ArrayHashMap<ko.Subscription>; // collection id -> ko subs
|
|
private databaseCollectionIdMap: ArrayHashMap<string>; // database id -> collection ids
|
|
|
|
public constructor(private container: Explorer) {
|
|
this.parameters = ko.observable(Date.now());
|
|
|
|
this.container.selectedNode.subscribe((newValue: any) => this.triggerRender());
|
|
this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender());
|
|
this.container.isNotebookEnabled.subscribe(newValue => this.triggerRender());
|
|
|
|
this.koSubsDatabaseIdMap = new ArrayHashMap();
|
|
this.koSubsCollectionIdMap = new ArrayHashMap();
|
|
this.databaseCollectionIdMap = new ArrayHashMap();
|
|
|
|
this.container.nonSystemDatabases.subscribe((databases: ViewModels.Database[]) => {
|
|
// Clean up old databases
|
|
this.cleanupDatabasesKoSubs(databases.map((database: ViewModels.Database) => database.id()));
|
|
|
|
databases.forEach((database: ViewModels.Database) => this.watchDatabase(database));
|
|
this.triggerRender();
|
|
});
|
|
|
|
this.container.nonSystemDatabases().forEach((database: ViewModels.Database) => this.watchDatabase(database));
|
|
this.triggerRender();
|
|
}
|
|
|
|
public renderComponent(): JSX.Element {
|
|
const dataRootNode = this.buildDataTree();
|
|
const notebooksRootNode = this.buildNotebooksTrees();
|
|
|
|
if (this.container.isNotebookEnabled()) {
|
|
return (
|
|
<>
|
|
<AccordionComponent>
|
|
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
|
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
|
</AccordionItemComponent>
|
|
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}>
|
|
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
|
|
</AccordionItemComponent>
|
|
</AccordionComponent>
|
|
|
|
{this.galleryContentRoot && this.buildGalleryCallout()}
|
|
</>
|
|
);
|
|
} else {
|
|
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
|
}
|
|
}
|
|
|
|
public async initialize(): Promise<void[]> {
|
|
const refreshTasks: Promise<void>[] = [];
|
|
|
|
this.galleryContentRoot = {
|
|
name: "Gallery",
|
|
path: "Gallery",
|
|
type: NotebookContentItemType.File
|
|
};
|
|
|
|
this.myNotebooksContentRoot = {
|
|
name: "My Notebooks",
|
|
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())
|
|
);
|
|
}
|
|
|
|
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
|
this.gitHubNotebooksContentRoot = {
|
|
name: "GitHub repos",
|
|
path: ResourceTreeAdapter.PseudoDirPath,
|
|
type: NotebookContentItemType.Directory
|
|
};
|
|
} else {
|
|
this.gitHubNotebooksContentRoot = undefined;
|
|
}
|
|
|
|
return Promise.all(refreshTasks);
|
|
}
|
|
|
|
public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void {
|
|
if (this.gitHubNotebooksContentRoot) {
|
|
this.gitHubNotebooksContentRoot.children = [];
|
|
pinnedRepos?.forEach(pinnedRepo => {
|
|
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
|
const repoTreeItem: NotebookContentItem = {
|
|
name: repoFullName,
|
|
path: ResourceTreeAdapter.PseudoDirPath,
|
|
type: NotebookContentItemType.Directory,
|
|
children: []
|
|
};
|
|
|
|
pinnedRepo.branches.forEach(branch => {
|
|
repoTreeItem.children.push({
|
|
name: branch.name,
|
|
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
|
|
type: NotebookContentItemType.Directory
|
|
});
|
|
});
|
|
|
|
this.gitHubNotebooksContentRoot.children.push(repoTreeItem);
|
|
});
|
|
|
|
this.triggerRender();
|
|
}
|
|
}
|
|
|
|
private buildDataTree(): TreeNode {
|
|
const databaseTreeNodes: TreeNode[] = this.container.nonSystemDatabases().map((database: ViewModels.Database) => {
|
|
const databaseNode: TreeNode = {
|
|
label: database.id(),
|
|
iconSrc: CosmosDBIcon,
|
|
isExpanded: false,
|
|
className: "databaseHeader",
|
|
children: [],
|
|
isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined),
|
|
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database),
|
|
onClick: isExpanded => {
|
|
// Rewritten version of expandCollapseDatabase():
|
|
if (!isExpanded) {
|
|
database.expandDatabase();
|
|
database.loadCollections();
|
|
} else {
|
|
database.collapseDatabase();
|
|
}
|
|
database.selectDatabase();
|
|
this.container.onUpdateTabsButtons([]);
|
|
this.container.tabsManager.refreshActiveTab(
|
|
(tab: TabsBase) => tab.collection && tab.collection.getDatabase().rid === database.rid
|
|
);
|
|
},
|
|
onContextMenuOpen: () => this.container.selectedNode(database)
|
|
};
|
|
|
|
if (database.isDatabaseShared()) {
|
|
databaseNode.children.push({
|
|
label: "Scale",
|
|
isSelected: () =>
|
|
this.isDataNodeSelected(database.rid, "Database", ViewModels.CollectionTabKind.DatabaseSettings),
|
|
onClick: database.onSettingsClick.bind(database)
|
|
});
|
|
}
|
|
|
|
// Find collections
|
|
database
|
|
.collections()
|
|
.forEach((collection: ViewModels.Collection) =>
|
|
databaseNode.children.push(this.buildCollectionNode(database, collection))
|
|
);
|
|
|
|
return databaseNode;
|
|
});
|
|
|
|
return {
|
|
label: undefined,
|
|
isExpanded: true,
|
|
children: databaseTreeNodes
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This is a rewrite of Collection.ts : showScriptsMenu, showStoredProcedures, showTriggers, showUserDefinedFunctions
|
|
* @param container
|
|
*/
|
|
private static showScriptNodes(container: Explorer): boolean {
|
|
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
|
|
}
|
|
|
|
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {
|
|
const children: TreeNode[] = [];
|
|
children.push({
|
|
label: collection.getLabel(),
|
|
onClick: () => {
|
|
collection.openTab();
|
|
// push to most recent
|
|
this.container.mostRecentActivity.addItem(CosmosClient.databaseAccount().id, {
|
|
type: MostRecentActivity.Type.OpenCollection,
|
|
title: collection.id(),
|
|
description: "Data",
|
|
data: collection.rid
|
|
});
|
|
},
|
|
isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Documents),
|
|
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection)
|
|
});
|
|
|
|
children.push({
|
|
label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings",
|
|
onClick: collection.onSettingsClick.bind(collection),
|
|
isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Settings)
|
|
});
|
|
|
|
if (ResourceTreeAdapter.showScriptNodes(this.container)) {
|
|
children.push(this.buildStoredProcedureNode(collection));
|
|
children.push(this.buildUserDefinedFunctionsNode(collection));
|
|
children.push(this.buildTriggerNode(collection));
|
|
}
|
|
|
|
// This is a rewrite of showConflicts
|
|
const showConflicts =
|
|
this.container.databaseAccount &&
|
|
this.container.databaseAccount() &&
|
|
this.container.databaseAccount().properties &&
|
|
this.container.databaseAccount().properties.enableMultipleWriteLocations &&
|
|
collection.rawDataModel &&
|
|
!!collection.rawDataModel.conflictResolutionPolicy;
|
|
|
|
if (showConflicts) {
|
|
children.push({
|
|
label: "Conflicts",
|
|
onClick: collection.onConflictsClick.bind(collection),
|
|
isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Conflicts)
|
|
});
|
|
}
|
|
|
|
return {
|
|
label: collection.id(),
|
|
iconSrc: CollectionIcon,
|
|
isExpanded: false,
|
|
children: children,
|
|
className: "collectionHeader",
|
|
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection),
|
|
onClick: () => {
|
|
// Rewritten version of expandCollapseCollection
|
|
this.container.selectedNode(collection);
|
|
this.container.onUpdateTabsButtons([]);
|
|
this.container.tabsManager.refreshActiveTab(
|
|
(tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid
|
|
);
|
|
},
|
|
onExpanded: () => {
|
|
if (ResourceTreeAdapter.showScriptNodes(this.container)) {
|
|
collection.loadStoredProcedures();
|
|
collection.loadUserDefinedFunctions();
|
|
collection.loadTriggers();
|
|
}
|
|
},
|
|
isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", undefined),
|
|
onContextMenuOpen: () => this.container.selectedNode(collection)
|
|
};
|
|
}
|
|
|
|
private buildStoredProcedureNode(collection: ViewModels.Collection): TreeNode {
|
|
return {
|
|
label: "Stored Procedures",
|
|
children: collection.storedProcedures().map((sp: StoredProcedure) => ({
|
|
label: sp.id(),
|
|
onClick: sp.open.bind(sp),
|
|
isSelected: () =>
|
|
this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.StoredProcedures),
|
|
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp)
|
|
})),
|
|
onClick: () => {
|
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
|
|
this.container.tabsManager.refreshActiveTab(
|
|
(tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid
|
|
);
|
|
}
|
|
};
|
|
}
|
|
|
|
private buildUserDefinedFunctionsNode(collection: ViewModels.Collection): TreeNode {
|
|
return {
|
|
label: "User Defined Functions",
|
|
children: collection.userDefinedFunctions().map((udf: UserDefinedFunction) => ({
|
|
label: udf.id(),
|
|
onClick: udf.open.bind(udf),
|
|
isSelected: () =>
|
|
this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.UserDefinedFunctions),
|
|
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(this.container, udf)
|
|
})),
|
|
onClick: () => {
|
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
|
|
this.container.tabsManager.refreshActiveTab(
|
|
(tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid
|
|
);
|
|
}
|
|
};
|
|
}
|
|
|
|
private buildTriggerNode(collection: ViewModels.Collection): TreeNode {
|
|
return {
|
|
label: "Triggers",
|
|
children: collection.triggers().map((trigger: Trigger) => ({
|
|
label: trigger.id(),
|
|
onClick: trigger.open.bind(trigger),
|
|
isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Triggers),
|
|
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger)
|
|
})),
|
|
onClick: () => {
|
|
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
|
|
this.container.tabsManager.refreshActiveTab(
|
|
(tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid
|
|
);
|
|
}
|
|
};
|
|
}
|
|
|
|
private buildNotebooksTrees(): TreeNode {
|
|
let notebooksTree: TreeNode = {
|
|
label: undefined,
|
|
isExpanded: true,
|
|
children: []
|
|
};
|
|
|
|
if (this.galleryContentRoot) {
|
|
notebooksTree.children.push(this.buildGalleryNotebooksTree());
|
|
}
|
|
|
|
if (this.myNotebooksContentRoot) {
|
|
notebooksTree.children.push(this.buildMyNotebooksTree());
|
|
}
|
|
|
|
if (this.gitHubNotebooksContentRoot) {
|
|
// collapse all other notebook nodes
|
|
notebooksTree.children.forEach(node => (node.isExpanded = false));
|
|
notebooksTree.children.push(this.buildGitHubNotebooksTree());
|
|
}
|
|
|
|
return notebooksTree;
|
|
}
|
|
|
|
private buildGalleryCallout(): JSX.Element {
|
|
if (
|
|
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
|
|
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
const calloutProps: ICalloutProps = {
|
|
calloutMaxWidth: 350,
|
|
ariaLabel: "New gallery",
|
|
role: "alertdialog",
|
|
gapSpace: 0,
|
|
target: ".galleryHeader",
|
|
directionalHint: DirectionalHint.leftTopEdge,
|
|
onDismiss: () => {
|
|
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
|
this.triggerRender();
|
|
},
|
|
setInitialFocus: true
|
|
};
|
|
|
|
const openGalleryProps: ILinkProps = {
|
|
onClick: () => {
|
|
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
|
this.container.openGallery();
|
|
this.triggerRender();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Callout {...calloutProps}>
|
|
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
|
<Text variant="xLarge" block>
|
|
New gallery
|
|
</Text>
|
|
<Text block>
|
|
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
|
contributors.
|
|
</Text>
|
|
<Link {...openGalleryProps}>Open gallery</Link>
|
|
</Stack>
|
|
</Callout>
|
|
);
|
|
}
|
|
|
|
private buildGalleryNotebooksTree(): TreeNode {
|
|
return {
|
|
label: "Gallery",
|
|
iconSrc: GalleryIcon,
|
|
className: "notebookHeader galleryHeader",
|
|
onClick: () => this.container.openGallery(),
|
|
isSelected: () => {
|
|
const activeTab = this.container.tabsManager.activeTab();
|
|
return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery;
|
|
}
|
|
};
|
|
}
|
|
|
|
private buildMyNotebooksTree(): TreeNode {
|
|
const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
|
|
this.myNotebooksContentRoot,
|
|
(item: NotebookContentItem) => {
|
|
this.container.openNotebook(item).then(hasOpened => {
|
|
if (hasOpened) {
|
|
this.pushItemToMostRecent(item);
|
|
}
|
|
});
|
|
},
|
|
true,
|
|
true
|
|
);
|
|
|
|
myNotebooksTree.isExpanded = true;
|
|
myNotebooksTree.isAlphaSorted = true;
|
|
// Remove "Delete" menu item from context menu
|
|
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter(menuItem => menuItem.label !== "Delete");
|
|
return myNotebooksTree;
|
|
}
|
|
|
|
private buildGitHubNotebooksTree(): TreeNode {
|
|
const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
|
|
this.gitHubNotebooksContentRoot,
|
|
(item: NotebookContentItem) => {
|
|
this.container.openNotebook(item).then(hasOpened => {
|
|
if (hasOpened) {
|
|
this.pushItemToMostRecent(item);
|
|
}
|
|
});
|
|
},
|
|
true,
|
|
true
|
|
);
|
|
|
|
gitHubNotebooksTree.contextMenu = [
|
|
{
|
|
label: "Manage GitHub settings",
|
|
onClick: () => this.container.gitHubReposPane.open()
|
|
},
|
|
{
|
|
label: "Disconnect from GitHub",
|
|
onClick: () => {
|
|
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
|
|
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
|
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
|
dataExplorerArea: Areas.Notebook
|
|
});
|
|
this.container.notebookManager?.gitHubOAuthService.logout();
|
|
}
|
|
}
|
|
];
|
|
|
|
gitHubNotebooksTree.isExpanded = true;
|
|
gitHubNotebooksTree.isAlphaSorted = true;
|
|
|
|
return gitHubNotebooksTree;
|
|
}
|
|
|
|
private pushItemToMostRecent(item: NotebookContentItem) {
|
|
this.container.mostRecentActivity.addItem(CosmosClient.databaseAccount().id, {
|
|
type: MostRecentActivity.Type.OpenNotebook,
|
|
title: item.name,
|
|
description: "Notebook",
|
|
data: {
|
|
name: item.name,
|
|
path: item.path
|
|
}
|
|
});
|
|
}
|
|
|
|
private buildChildNodes(
|
|
item: NotebookContentItem,
|
|
onFileClick: (item: NotebookContentItem) => void,
|
|
createDirectoryContextMenu: boolean,
|
|
createFileContextMenu: boolean
|
|
): TreeNode[] {
|
|
if (!item || !item.children) {
|
|
return [];
|
|
} else {
|
|
return item.children.map(item => {
|
|
const result =
|
|
item.type === NotebookContentItemType.Directory
|
|
? this.buildNotebookDirectoryNode(item, onFileClick, createDirectoryContextMenu, createFileContextMenu)
|
|
: this.buildNotebookFileNode(item, onFileClick, createFileContextMenu);
|
|
result.timestamp = item.timestamp;
|
|
return result;
|
|
});
|
|
}
|
|
}
|
|
|
|
private buildNotebookFileNode(
|
|
item: NotebookContentItem,
|
|
onFileClick: (item: NotebookContentItem) => void,
|
|
createFileContextMenu: boolean
|
|
): TreeNode {
|
|
return {
|
|
label: item.name,
|
|
iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon,
|
|
className: "notebookHeader",
|
|
onClick: () => onFileClick(item),
|
|
isSelected: () => {
|
|
const activeTab = this.container.tabsManager.activeTab();
|
|
return (
|
|
activeTab &&
|
|
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
|
/* 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.
|
|
*/
|
|
(activeTab as any).notebookPath() === item.path
|
|
);
|
|
},
|
|
contextMenu: createFileContextMenu
|
|
? [
|
|
{
|
|
label: "Rename",
|
|
iconSrc: NotebookIcon,
|
|
onClick: () => this.container.renameNotebook(item)
|
|
},
|
|
{
|
|
label: "Delete",
|
|
iconSrc: DeleteIcon,
|
|
onClick: () => {
|
|
this.container.showOkCancelModalDialog(
|
|
"Confirm delete",
|
|
`Are you sure you want to delete "${item.name}"`,
|
|
"Delete",
|
|
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
|
|
"Cancel",
|
|
undefined
|
|
);
|
|
}
|
|
},
|
|
{
|
|
label: "Download",
|
|
iconSrc: NotebookIcon,
|
|
onClick: () => this.container.downloadFile(item)
|
|
}
|
|
]
|
|
: undefined,
|
|
data: item
|
|
};
|
|
}
|
|
|
|
private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
|
|
let items: TreeNodeMenuItem[] = [
|
|
{
|
|
label: "Refresh",
|
|
iconSrc: RefreshIcon,
|
|
onClick: () => this.container.refreshContentItem(item).then(() => this.triggerRender())
|
|
},
|
|
{
|
|
label: "Delete",
|
|
iconSrc: DeleteIcon,
|
|
onClick: () => {
|
|
this.container.showOkCancelModalDialog(
|
|
"Confirm delete",
|
|
`Are you sure you want to delete "${item.name}?"`,
|
|
"Delete",
|
|
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
|
|
"Cancel",
|
|
undefined
|
|
);
|
|
}
|
|
},
|
|
{
|
|
label: "Rename",
|
|
iconSrc: NotebookIcon,
|
|
onClick: () => this.container.renameNotebook(item).then(() => this.triggerRender())
|
|
},
|
|
{
|
|
label: "New Directory",
|
|
iconSrc: NewNotebookIcon,
|
|
onClick: () => this.container.onCreateDirectory(item)
|
|
},
|
|
{
|
|
label: "New Notebook",
|
|
iconSrc: NewNotebookIcon,
|
|
onClick: () => this.container.onNewNotebookClicked(item)
|
|
},
|
|
{
|
|
label: "Upload File",
|
|
iconSrc: NewNotebookIcon,
|
|
onClick: () => this.container.onUploadToNotebookServerClicked(item)
|
|
}
|
|
];
|
|
|
|
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
|
|
if (GitHubUtils.fromContentUri(item.path)) {
|
|
items = items.filter(
|
|
item =>
|
|
item.label !== "Delete" &&
|
|
item.label !== "Rename" &&
|
|
item.label !== "New Directory" &&
|
|
item.label !== "Upload File"
|
|
);
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
private buildNotebookDirectoryNode(
|
|
item: NotebookContentItem,
|
|
onFileClick: (item: NotebookContentItem) => void,
|
|
createDirectoryContextMenu: boolean,
|
|
createFileContextMenu: boolean
|
|
): TreeNode {
|
|
return {
|
|
label: item.name,
|
|
iconSrc: undefined,
|
|
className: "notebookHeader",
|
|
isAlphaSorted: true,
|
|
isLeavesParentsSeparate: true,
|
|
onClick: () => {
|
|
if (!item.children) {
|
|
this.container.refreshContentItem(item).then(() => this.triggerRender());
|
|
}
|
|
},
|
|
isSelected: () => {
|
|
const activeTab = this.container.tabsManager.activeTab();
|
|
return (
|
|
activeTab &&
|
|
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
|
|
/* 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.
|
|
*/
|
|
(activeTab as any).notebookPath() === item.path
|
|
);
|
|
},
|
|
contextMenu:
|
|
createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath
|
|
? this.createDirectoryContextMenu(item)
|
|
: undefined,
|
|
data: item,
|
|
children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu)
|
|
};
|
|
}
|
|
|
|
public triggerRender() {
|
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
|
}
|
|
|
|
private isDataNodeSelected(rid: string, nodeKind: string, subnodeKind: ViewModels.CollectionTabKind): boolean {
|
|
if (!this.container.selectedNode || !this.container.selectedNode()) {
|
|
return false;
|
|
}
|
|
const selectedNode = this.container.selectedNode();
|
|
|
|
if (subnodeKind === undefined) {
|
|
return selectedNode.rid === rid && selectedNode.nodeKind === nodeKind;
|
|
} else {
|
|
const activeTab = this.container.tabsManager.activeTab();
|
|
let selectedSubnodeKind;
|
|
if (nodeKind === "Database" && (selectedNode as ViewModels.Database).selectedSubnodeKind) {
|
|
selectedSubnodeKind = (selectedNode as ViewModels.Database).selectedSubnodeKind();
|
|
} else if (nodeKind === "Collection" && (selectedNode as ViewModels.Collection).selectedSubnodeKind) {
|
|
selectedSubnodeKind = (selectedNode as ViewModels.Collection).selectedSubnodeKind();
|
|
}
|
|
|
|
return (
|
|
activeTab &&
|
|
activeTab.tabKind === subnodeKind &&
|
|
selectedNode.rid === rid &&
|
|
selectedSubnodeKind !== undefined &&
|
|
selectedSubnodeKind === subnodeKind
|
|
);
|
|
}
|
|
}
|
|
|
|
// *************** watch all nested ko's inside database
|
|
// TODO Simplify so we don't have to do this
|
|
|
|
private watchCollection(databaseId: string, collection: ViewModels.Collection) {
|
|
this.addKoSubToCollectionId(
|
|
databaseId,
|
|
collection.id(),
|
|
collection.storedProcedures.subscribe(() => {
|
|
this.triggerRender();
|
|
})
|
|
);
|
|
|
|
this.addKoSubToCollectionId(
|
|
databaseId,
|
|
collection.id(),
|
|
collection.isCollectionExpanded.subscribe(() => {
|
|
this.triggerRender();
|
|
})
|
|
);
|
|
|
|
this.addKoSubToCollectionId(
|
|
databaseId,
|
|
collection.id(),
|
|
collection.isStoredProceduresExpanded.subscribe(() => {
|
|
this.triggerRender();
|
|
})
|
|
);
|
|
}
|
|
|
|
private watchDatabase(database: ViewModels.Database) {
|
|
const databaseId = database.id();
|
|
const koSub = database.collections.subscribe((collections: ViewModels.Collection[]) => {
|
|
this.cleanupCollectionsKoSubs(
|
|
databaseId,
|
|
collections.map((collection: ViewModels.Collection) => collection.id())
|
|
);
|
|
|
|
collections.forEach((collection: ViewModels.Collection) => this.watchCollection(databaseId, collection));
|
|
this.triggerRender();
|
|
});
|
|
this.addKoSubToDatabaseId(databaseId, koSub);
|
|
|
|
database.collections().forEach((collection: ViewModels.Collection) => this.watchCollection(databaseId, collection));
|
|
}
|
|
|
|
private addKoSubToDatabaseId(databaseId: string, sub: ko.Subscription): void {
|
|
this.koSubsDatabaseIdMap.push(databaseId, sub);
|
|
}
|
|
|
|
private addKoSubToCollectionId(databaseId: string, collectionId: string, sub: ko.Subscription): void {
|
|
this.databaseCollectionIdMap.push(databaseId, collectionId);
|
|
this.koSubsCollectionIdMap.push(collectionId, sub);
|
|
}
|
|
|
|
private cleanupDatabasesKoSubs(existingDatabaseIds: string[]): void {
|
|
const databaseIdsToRemove = this.databaseCollectionIdMap
|
|
.keys()
|
|
.filter((id: string) => existingDatabaseIds.indexOf(id) === -1);
|
|
|
|
databaseIdsToRemove.forEach((databaseId: string) => {
|
|
if (this.koSubsDatabaseIdMap.has(databaseId)) {
|
|
this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose());
|
|
this.koSubsDatabaseIdMap.delete(databaseId);
|
|
}
|
|
|
|
if (this.databaseCollectionIdMap.has(databaseId)) {
|
|
this.databaseCollectionIdMap
|
|
.get(databaseId)
|
|
.forEach((collectionId: string) => this.cleanupKoSubsForCollection(databaseId, collectionId));
|
|
}
|
|
});
|
|
}
|
|
|
|
private cleanupCollectionsKoSubs(databaseId: string, existingCollectionIds: string[]): void {
|
|
if (!this.databaseCollectionIdMap.has(databaseId)) {
|
|
return;
|
|
}
|
|
|
|
const collectionIdsToRemove = this.databaseCollectionIdMap
|
|
.get(databaseId)
|
|
.filter((id: string) => existingCollectionIds.indexOf(id) === -1);
|
|
|
|
collectionIdsToRemove.forEach((id: string) => this.cleanupKoSubsForCollection(databaseId, id));
|
|
}
|
|
|
|
private cleanupKoSubsForCollection(databaseId: string, collectionId: string) {
|
|
if (!this.koSubsCollectionIdMap.has(collectionId)) {
|
|
return;
|
|
}
|
|
|
|
this.koSubsCollectionIdMap.get(collectionId).forEach((sub: ko.Subscription) => sub.dispose());
|
|
this.koSubsCollectionIdMap.delete(collectionId);
|
|
this.databaseCollectionIdMap.remove(databaseId, collectionId);
|
|
}
|
|
}
|