Create tabs manager and refactor tab related logic (#66)

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
This commit is contained in:
victor-meng 2020-07-09 13:53:37 -07:00 committed by GitHub
parent 326bd4f494
commit 4068a9fbaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 3430 additions and 3139 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2349,6 +2349,11 @@ a:link {
text-decoration: none;
}
.tabsContainer {
height: 100%;
overflow: hidden;
}
.tabs {
position: relative;
margin: 15px 0 25px 0;

2816
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@ import { QueryMetrics } from "@azure/cosmos";
import { SetupNotebooksPane } from "../Explorer/Panes/SetupNotebooksPane";
import { Splitter } from "../Common/Splitter";
import { StringInputPane } from "../Explorer/Panes/StringInputPane";
import { TabsManager } from "../Explorer/Tabs/TabsManager";
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
import { UploadDetails } from "../workers/upload/definitions";
import { UploadItemsPaneAdapter } from "../Explorer/Panes/UploadItemsPaneAdapter";
@ -121,9 +122,8 @@ export interface Explorer {
isResourceTokenCollectionNodeSelected: ko.Computed<boolean>;
// Tabs
openedTabs: ko.ObservableArray<Tab>;
activeTab: ko.Observable<Tab>;
isTabsContentExpanded: ko.Observable<boolean>;
tabsManager: TabsManager;
// Contextual Panes
addDatabasePane: AddDatabasePane;
@ -161,8 +161,6 @@ export interface Explorer {
refreshDatabaseForResourceToken(): Q.Promise<void>;
refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any>;
closeAllPanes(): void;
closeAllTabsForResource(resourceId: string): void;
findActiveTab(): Tab; // TODO Deprecate in favor activeTab
findSelectedDatabase(): Database;
findDatabaseWithId(databaseRid: string): Database;
isLastDatabase(): boolean;
@ -221,7 +219,6 @@ export interface Explorer {
sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
arcadiaToken: ko.Observable<string>;
arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
isNotebookTabActive: ko.Computed<boolean>;
memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
notebookManager?: any; // This is dynamically loaded
openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean>; // True if it was opened, false otherwise
@ -250,7 +247,6 @@ export interface Explorer {
readFile: (notebookFile: NotebookContentItem) => Promise<string>;
downloadFile: (notebookFile: NotebookContentItem) => Promise<void>;
createNotebookContentItemFile: (name: string, filepath: string) => NotebookContentItem;
closeNotebookTab: (filepath: string) => void;
refreshContentItem(item: NotebookContentItem): Promise<void>;
getNotebookBasePath(): string;
@ -381,7 +377,6 @@ export interface Database extends TreeNode {
findCollectionWithId(collectionRid: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void;
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
refreshTabSelectedState(): void;
readSettings(): void;
onSettingsClick: () => void;
}
@ -403,7 +398,6 @@ export interface CollectionBase extends TreeNode {
onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void;
expandCollection(): Q.Promise<any>;
collapseCollection(): void;
refreshActiveTab(): void;
getDatabase(): Database;
}
@ -837,7 +831,6 @@ export interface TabOptions {
onUpdateTabsButtons: (buttons: NavbarButtonConfig[]) => void;
isTabsContentExpanded?: ko.Observable<boolean>;
onLoadStartKey?: number;
openedTabs: Tab[];
// TODO Remove the flag and use a context to handle this
// TODO: 145357 Remove dependency on collection/database and add abstraction
@ -931,14 +924,12 @@ export interface Tab {
tabTitle: ko.Observable<string>;
hashLocation: ko.Observable<string>;
closeTabButton: Button;
onCloseTabButtonClick(): Q.Promise<any>;
onCloseTabButtonClick(): void;
onTabClick(): Q.Promise<any>;
onKeyPressActivate(source: any, event: KeyboardEvent): void;
onKeyPressClose(source: any, event: KeyboardEvent): void;
onActivate(): Q.Promise<any>;
refresh(): void;
nextTab: ko.Observable<Tab>;
previousTab: ko.Observable<Tab>;
closeButtonTabIndex: ko.Computed<number>;
isExecutionError: ko.Observable<boolean>;
isExecuting: ko.Observable<boolean>;

View File

@ -10,6 +10,7 @@ import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleCompo
import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead";
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import { TabsManagerKOComponent } from "./Tabs/TabsManager";
import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { ToolbarComponent } from "./Controls/Toolbar/Toolbar";
@ -26,6 +27,7 @@ ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input", ThroughputInputComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
ko.components.register("tabs-manager", TabsManagerKOComponent());
// Collection Tabs
ko.components.register("documents-tab", new TabComponents.DocumentsTab());

View File

@ -77,6 +77,7 @@ import { SplashScreenComponentAdapter } from "./SplashScreen/SplashScreenCompone
import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter";
import { StringInputPane } from "./Panes/StringInputPane";
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
import { TabsManager } from "./Tabs/TabsManager";
import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane";
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
@ -161,11 +162,10 @@ export default class Explorer implements ViewModels.Explorer {
private resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
// Tabs
public openedTabs: ko.ObservableArray<ViewModels.Tab>;
public activeTab: ko.Observable<ViewModels.Tab>;
public isTabsContentExpanded: ko.Observable<boolean>;
public galleryTab: any;
public notebookViewerTab: any;
public tabsManager: TabsManager;
// Contextual panes
public addDatabasePane: ViewModels.AddDatabasePane;
@ -229,7 +229,6 @@ export default class Explorer implements ViewModels.Explorer {
public arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
public isSynapseLinkUpdating: ko.Observable<boolean>;
public isNotebookTabActive: ko.Computed<boolean>;
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
public notebookManager?: any; // This is dynamically loaded
@ -296,13 +295,9 @@ export default class Explorer implements ViewModels.Explorer {
this.arcadiaToken = ko.observable<string>();
this.arcadiaToken.subscribe((token: string) => {
if (token) {
this.openedTabs &&
this.openedTabs().forEach(tab => {
if (tab.tabKind === ViewModels.CollectionTabKind.Notebook) {
throw new Error("NotebookTab is deprecated. Use NotebookV2Tab");
} else if (tab.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
(tab as NotebookV2Tab).reconfigureServiceEndpoints();
}
const notebookTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2);
(notebookTabs || []).forEach((tab: NotebookV2Tab) => {
tab.reconfigureServiceEndpoints();
});
}
});
@ -771,12 +766,8 @@ export default class Explorer implements ViewModels.Explorer {
container: this
});
this.openedTabs = ko.observableArray<ViewModels.Tab>([]);
this.activeTab = ko.observable(null);
this.isNotebookTabActive = ko.computed<boolean>(() => {
// only show memory tracker if the current active tab is a notebook
return this.activeTab() && this.activeTab().tabKind === ViewModels.CollectionTabKind.NotebookV2;
});
this.tabsManager = new TabsManager();
this._panes = [
this.addDatabasePane,
this.addCollectionPane,
@ -1262,7 +1253,7 @@ export default class Explorer implements ViewModels.Explorer {
class: "connectDialogButtons okBtn connectOkBtns",
click: () => {
$("#contextSwitchPrompt").dialog("close");
this.openedTabs([]); // clear all tabs so we dont leave any tabs from previous session open
this.tabsManager.closeTabs(); // clear all tabs so we dont leave any tabs from previous session open
this.renewShareAccess(connectionString);
}
};
@ -2090,10 +2081,6 @@ export default class Explorer implements ViewModels.Explorer {
return Q();
}
public findActiveTab(): ViewModels.Tab {
return this.activeTab();
}
public findSelectedCollection(): ViewModels.Collection {
if (this.selectedNode().nodeKind === "Collection") {
return this.findSelectedCollectionForSelectedNode();
@ -2106,11 +2093,9 @@ export default class Explorer implements ViewModels.Explorer {
public findSelectedStoredProcedure(): ViewModels.StoredProcedure {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.storedProcedures(), (storedProcedure: ViewModels.StoredProcedure) => {
const openedSprocTab = this.openedTabs().filter(
(tab: ViewModels.Tab) =>
tab.node &&
tab.node.rid === storedProcedure.rid &&
tab.tabKind === ViewModels.CollectionTabKind.StoredProcedures
const openedSprocTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.StoredProcedures,
(tab: ViewModels.Tab) => tab.node && tab.node.rid === storedProcedure.rid
);
return (
storedProcedure.rid === this.selectedNode().rid ||
@ -2122,11 +2107,9 @@ export default class Explorer implements ViewModels.Explorer {
public findSelectedUDF(): ViewModels.UserDefinedFunction {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: ViewModels.UserDefinedFunction) => {
const openedUdfTab = this.openedTabs().filter(
(tab: ViewModels.Tab) =>
tab.node &&
tab.node.rid === userDefinedFunction.rid &&
tab.tabKind === ViewModels.CollectionTabKind.UserDefinedFunctions
const openedUdfTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.UserDefinedFunctions,
(tab: ViewModels.Tab) => tab.node && tab.node.rid === userDefinedFunction.rid
);
return (
userDefinedFunction.rid === this.selectedNode().rid ||
@ -2138,9 +2121,9 @@ export default class Explorer implements ViewModels.Explorer {
public findSelectedTrigger(): ViewModels.Trigger {
const selectedCollection: ViewModels.Collection = this.findSelectedCollection();
return _.find(selectedCollection.triggers(), (trigger: ViewModels.Trigger) => {
const openedTriggerTab = this.openedTabs().filter(
(tab: ViewModels.Tab) =>
tab.node && tab.node.rid === trigger.rid && tab.tabKind === ViewModels.CollectionTabKind.Triggers
const openedTriggerTab = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Triggers,
(tab: ViewModels.Tab) => tab.node && tab.node.rid === trigger.rid
);
return (
trigger.rid === this.selectedNode().rid ||
@ -2149,16 +2132,6 @@ export default class Explorer implements ViewModels.Explorer {
});
}
public closeAllTabsForResource(resourceId: string): void {
const currentlyActiveTabs = this.openedTabs().filter((tab: ViewModels.Tab) => tab.isActive && tab.isActive());
currentlyActiveTabs.forEach((tab: ViewModels.Tab) => tab.isActive(false));
this.activeTab(null);
const openedTabsForResource = this.openedTabs().filter(
(tab: ViewModels.Tab) => tab.node && tab.node.rid === resourceId
);
openedTabsForResource.forEach((tab: ViewModels.Tab) => tab.onCloseTabButtonClick());
}
public closeAllPanes(): void {
this._panes.forEach((pane: ViewModels.ContextualPane) => pane.close());
}
@ -2235,7 +2208,9 @@ export default class Explorer implements ViewModels.Explorer {
if (isNewDatabase) {
database.expandDatabase();
}
database.refreshTabSelectedState();
this.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.getDatabase().rid === database.rid
);
})
);
});
@ -2338,7 +2313,7 @@ export default class Explorer implements ViewModels.Explorer {
}
const urlPrefixWithKeyParam: string = `${config.hostedExplorerURL}?key=`;
const currentActiveTab: ViewModels.Tab = this.findActiveTab();
const currentActiveTab: ViewModels.Tab = this.tabsManager.activeTab();
return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`;
}
@ -2600,14 +2575,18 @@ export default class Explorer implements ViewModels.Explorer {
if (!notebookContentItem || !notebookContentItem.path) {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
}
const openedNotebookTabs = this.getNotebookTabsForFilepath(notebookContentItem.path);
if (openedNotebookTabs.length > 0) {
openedNotebookTabs[0].onTabClick();
openedNotebookTabs[0].onActivate();
return true;
}
const notebookTabs: NotebookV2Tab[] = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: ViewModels.Tab) =>
(tab as NotebookV2Tab).notebookPath &&
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path)
) as NotebookV2Tab[];
let notebookTab: NotebookV2Tab = notebookTabs && notebookTabs[0];
if (notebookTab) {
this.tabsManager.activateTab(notebookTab);
} else {
const options: ViewModels.NotebookTabOptions = {
account: CosmosClient.databaseAccount(),
tabKind: ViewModels.CollectionTabKind.NotebookV2,
@ -2625,21 +2604,19 @@ export default class Explorer implements ViewModels.Explorer {
onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons,
container: this,
notebookContentItem,
openedTabs: this.openedTabs()
notebookContentItem
};
try {
const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab");
const notebookTab = new NotebookTabV2.default(options);
this.openedTabs.push(notebookTab);
// Activate
notebookTab.onTabClick();
notebookTab = new NotebookTabV2.default(options);
this.tabsManager.activateNewTab(notebookTab);
} catch (reason) {
console.error("Import NotebookV2Tab failed!", reason);
return false;
}
}
return true;
}
@ -2652,10 +2629,11 @@ export default class Explorer implements ViewModels.Explorer {
}
// Don't delete if tab is open to avoid accidental deletion
const openedNotebookTabs = this.openedTabs().filter(
(tab: ViewModels.Tab) =>
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
(tab as NotebookV2Tab).notebookPath() === notebookFile.path
const openedNotebookTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path);
}
);
if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
@ -2675,13 +2653,11 @@ export default class Explorer implements ViewModels.Explorer {
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
})
.then(newNotebookFile => {
this.openedTabs()
.filter(
(tab: ViewModels.Tab) =>
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), originalPath)
)
.forEach(tab => {
const notebookTabs: ViewModels.Tab[] = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
);
notebookTabs.forEach(tab => {
tab.tabTitle(newNotebookFile.name);
tab.tabPath(newNotebookFile.path);
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
@ -2865,16 +2841,15 @@ export default class Explorer implements ViewModels.Explorer {
await this.initSparkConnectionInfo(this.databaseAccount());
}
const openedSparkMasterTabs = this.openedTabs().filter(
(tab: ViewModels.Tab) => tab.tabKind === ViewModels.CollectionTabKind.SparkMasterTab
);
if (openedSparkMasterTabs.length > 0) {
openedSparkMasterTabs[0].onTabClick();
openedSparkMasterTabs[0].onActivate();
return;
}
const sparkMasterTabs: SparkMasterTab[] = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.SparkMasterTab
) as SparkMasterTab[];
let sparkMasterTab: SparkMasterTab = sparkMasterTabs && sparkMasterTabs[0];
const sparkMasterTab = new SparkMasterTab({
if (sparkMasterTab) {
this.tabsManager.activateTab(sparkMasterTab);
} else {
sparkMasterTab = new SparkMasterTab({
clusterConnectionInfo: this.sparkClusterConnectionInfo(),
tabKind: ViewModels.CollectionTabKind.SparkMasterTab,
node: null,
@ -2889,15 +2864,11 @@ export default class Explorer implements ViewModels.Explorer {
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons,
openedTabs: this.openedTabs(),
container: this
});
this.openedTabs.push(sparkMasterTab);
// Activate
sparkMasterTab.onTabClick();
return;
this.tabsManager.activateNewTab(sparkMasterTab);
}
}
private refreshNotebookList = async (): Promise<void> => {
@ -2920,9 +2891,11 @@ export default class Explorer implements ViewModels.Explorer {
}
// Don't delete if tab is open to avoid accidental deletion
const openedNotebookTabs = this.openedTabs().filter(
(tab: ViewModels.Tab) =>
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 && (tab as NotebookV2Tab).notebookPath() === item.path
const openedNotebookTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path);
}
);
if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
@ -3084,18 +3057,15 @@ export default class Explorer implements ViewModels.Explorer {
throw new Error("Terminal kind: ${kind} not supported");
}
const openedTabs = this.openedTabs().filter(
(tab: ViewModels.Tab) => tab.tabKind === ViewModels.CollectionTabKind.Terminal
);
for (let i = 0; i < openedTabs.length; ++i) {
if (openedTabs[i].hashLocation() == hashLocation) {
openedTabs[i].onTabClick();
openedTabs[i].onActivate();
return;
}
}
const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Terminal,
(tab: ViewModels.Tab) => tab.hashLocation() == hashLocation
) as TerminalTab[];
let terminalTab: TerminalTab = terminalTabs && terminalTabs[0];
if (terminalTab) {
this.tabsManager.activateTab(terminalTab);
} else {
const newTab = new TerminalTab({
account: CosmosClient.databaseAccount(),
tabKind: ViewModels.CollectionTabKind.Terminal,
@ -3112,36 +3082,27 @@ export default class Explorer implements ViewModels.Explorer {
onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons,
container: this,
openedTabs: this.openedTabs(),
kind: kind
});
this.openedTabs.push(newTab);
// Activate
newTab.onTabClick();
this.tabsManager.activateNewTab(newTab);
}
}
public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) {
let title: string;
let hashLocation: string;
let title: string = "Gallery";
let hashLocation: string = "gallery";
title = "Gallery";
hashLocation = "gallery";
const openedTabs = this.openedTabs().filter(
(tab: ViewModels.Tab) => tab.tabKind === ViewModels.CollectionTabKind.Gallery
const galleryTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Gallery,
(tab: ViewModels.Tab) => tab.hashLocation() == hashLocation
);
let galleryTab = galleryTabs && galleryTabs[0];
for (let i = 0; i < openedTabs.length; ++i) {
if (openedTabs[i].hashLocation() == hashLocation) {
openedTabs[i].onTabClick();
openedTabs[i].onActivate();
(openedTabs[i] as any).updateGalleryParams(notebookUrl, galleryItem, isFavorite);
return;
}
}
if (galleryTab) {
this.tabsManager.activateTab(galleryTab);
(galleryTab as any).updateGalleryParams(notebookUrl, galleryItem, isFavorite);
} else {
if (!this.galleryTab) {
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
}
@ -3164,14 +3125,11 @@ export default class Explorer implements ViewModels.Explorer {
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
openedTabs: this.openedTabs()
onLoadStartKey: null
});
this.openedTabs.push(newTab);
// Activate
newTab.onTabClick();
this.tabsManager.activateNewTab(newTab);
}
}
public async openNotebookViewer(notebookUrl: string) {
@ -3189,19 +3147,18 @@ export default class Explorer implements ViewModels.Explorer {
return notebookViewerTab.notebookUrl === notebookUrl;
};
const openedTabs = this.openedTabs().filter(
(tab: ViewModels.Tab) => tab.tabKind === ViewModels.CollectionTabKind.NotebookViewer && isNotebookViewerOpen(tab)
const notebookViewerTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: ViewModels.Tab) => {
return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab);
}
);
let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0];
for (let i = 0; i < openedTabs.length; ++i) {
if (openedTabs[i].hashLocation() == hashLocation) {
openedTabs[i].onTabClick();
openedTabs[i].onActivate();
return;
}
}
const newTab = new this.notebookViewerTab.default({
if (notebookViewerTab) {
this.tabsManager.activateNewTab(notebookViewerTab);
} else {
notebookViewerTab = new this.notebookViewerTab.default({
account: CosmosClient.databaseAccount(),
tabKind: ViewModels.CollectionTabKind.NotebookViewer,
node: null,
@ -3216,14 +3173,11 @@ export default class Explorer implements ViewModels.Explorer {
onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons,
container: this,
openedTabs: this.openedTabs(),
notebookUrl
});
this.openedTabs.push(newTab);
// Activate
newTab.onTabClick();
this.tabsManager.activateNewTab(notebookViewerTab);
}
}
public onNewCollectionClicked(): void {
@ -3235,24 +3189,8 @@ export default class Explorer implements ViewModels.Explorer {
document.getElementById("linkAddCollection").focus();
}
private getNotebookTabsForFilepath(filepath: string): ViewModels.Tab[] {
return this.openedTabs().filter(
(tab: ViewModels.Tab) =>
tab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
(tab as any).notebookPath &&
FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
}
public closeNotebookTab(filepath: string): void {
if (!filepath) {
return;
}
this.getNotebookTabsForFilepath(filepath).forEach(tab => tab.onCloseTabButtonClick());
}
private refreshCommandBarButtons(): void {
const activeTab = this.findActiveTab();
const activeTab = this.tabsManager.activeTab();
if (activeTab) {
activeTab.onActivate(); // TODO only update tabs buttons?
} else {

View File

@ -16,10 +16,14 @@ export class CommandBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
public container: ViewModels.Explorer;
private tabsButtons: ViewModels.NavbarButtonConfig[];
private isNotebookTabActive: ko.Computed<boolean>;
constructor(container: ViewModels.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 = [
@ -39,7 +43,7 @@ export class CommandBarComponentAdapter implements ReactAdapter {
container.isHostedDataExplorerEnabled,
container.isSynapseLinkUpdating,
container.databaseAccount,
container.isNotebookTabActive
this.isNotebookTabActive
];
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
@ -74,7 +78,7 @@ export class CommandBarComponentAdapter implements ReactAdapter {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (this.container && this.container.isNotebookTabActive()) {
if (this.isNotebookTabActive()) {
uiFabricControlButtons.unshift(
CommandBarUtil.createMemoryTracker("memoryTracker", this.container.memoryUsageInfo)
);

View File

@ -43,6 +43,7 @@ import { CdbAppState } from "./types";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
import * as TextFile from "./contents/file/text-file";
import { NotebookUtil } from "../NotebookUtil";
import { FileSystemUtil } from "../FileSystemUtil";
interface NotebookServiceConfig extends JupyterServerConfig {
userPuid?: string;
@ -806,7 +807,9 @@ const closeUnsupportedMimetypesEpic = (
if (explorer && !TextFile.handles(mimetype)) {
const filepath = action.payload.filepath;
// Close tab and show error message
explorer.closeNotebookTab(filepath);
explorer.tabsManager.closeTabsByComparator(
tab => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
explorer.showOkModalDialog("File cannot be rendered", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
@ -832,7 +835,9 @@ const closeContentFailedToFetchEpic = (
if (explorer) {
const filepath = action.payload.filepath;
// Close tab and show error message
explorer.closeNotebookTab(filepath);
explorer.tabsManager.closeTabsByComparator(
tab => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
const msg = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);

View File

@ -22,6 +22,7 @@ import { Versions } from "../../src/Contracts/ExplorerContracts";
import { CollectionCreationDefaults } from "../Shared/Constants";
import { IGalleryItem } from "../Juno/JunoClient";
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
import { TabsManager } from "./Tabs/TabsManager";
export class ExplorerStub implements ViewModels.Explorer {
public flight: ko.Observable<string>;
@ -69,7 +70,6 @@ export class ExplorerStub implements ViewModels.Explorer {
public isLeftPaneExpanded: ko.Observable<boolean>;
public selectedNode: ko.Observable<ViewModels.TreeNode>;
public isRefreshingExplorer: ko.Observable<boolean>;
public openedTabs: ko.ObservableArray<ViewModels.Tab>;
public isTabsContentExpanded: ko.Observable<boolean>;
public addCollectionPane: ViewModels.AddCollectionPane;
public addDatabasePane: ViewModels.AddDatabasePane;
@ -101,7 +101,6 @@ export class ExplorerStub implements ViewModels.Explorer {
public canExceedMaximumValue: ko.Computed<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>(Versions.DataExplorer);
public activeTab: ko.Observable<ViewModels.Tab>;
public mostRecentActivity: MostRecentActivity;
public isNotebookEnabled: ko.Observable<boolean>;
public isSparkEnabled: ko.Observable<boolean>;
@ -119,7 +118,6 @@ export class ExplorerStub implements ViewModels.Explorer {
public arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
public isSynapseLinkUpdating: ko.Observable<boolean>;
public isNotebookTabActive: ko.Computed<boolean>;
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
public notebookManager?: any;
public openGallery: (notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) => void;
@ -130,6 +128,7 @@ export class ExplorerStub implements ViewModels.Explorer {
public resourceTokenPartitionKey: ko.Observable<string>;
public isAuthWithResourceToken: ko.Observable<boolean>;
public isResourceTokenCollectionNodeSelected: ko.Computed<boolean>;
public tabsManager: TabsManager;
private _featureEnabledReturnValue: boolean;
@ -248,10 +247,6 @@ export class ExplorerStub implements ViewModels.Explorer {
throw new Error("Not implemented");
}
public findActiveTab(): ViewModels.Tab {
throw new Error("Not implemented");
}
public findSelectedStoredProcedure(): ViewModels.StoredProcedure {
throw new Error("Not implemented");
}
@ -300,10 +295,6 @@ export class ExplorerStub implements ViewModels.Explorer {
throw new Error("Not implemented");
}
public closeAllTabsForResource(resourceId: string): void {
throw new Error("Not implemented");
}
public getPlatformType(): PlatformType {
throw new Error("Not implemented");
}
@ -426,10 +417,6 @@ export class ExplorerStub implements ViewModels.Explorer {
throw new Error("Not implemented");
}
public closeNotebookTab(filepath: string): void {
throw new Error("Not implemented");
}
public refreshContentItem(item: NotebookContentItem): Promise<void> {
throw new Error("Not implemented");
}
@ -511,10 +498,6 @@ export class DatabaseStub implements ViewModels.Database {
throw new Error("Not implemented");
}
public refreshTabSelectedState(): void {
throw new Error("Not implemented");
}
public readSettings() {
throw new Error("Not implemented");
}
@ -779,10 +762,6 @@ export class CollectionStub implements ViewModels.Collection {
throw new Error("Not implemented");
};
public refreshActiveTab = (): void => {
throw new Error("Not implemented");
};
public getLabel(): string {
throw new Error("Not implemented");
}

View File

@ -87,7 +87,7 @@ export class BrowseQueriesPane extends ContextualPaneBase implements ViewModels.
} else {
selectedCollection.onNewQueryClick(selectedCollection, null);
}
const queryTab: ViewModels.QueryTab = this.container.findActiveTab() as ViewModels.QueryTab;
const queryTab: ViewModels.QueryTab = this.container.tabsManager.activeTab() as ViewModels.QueryTab;
queryTab.tabTitle(savedQuery.queryName);
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);
queryTab.initialEditorContent(savedQuery.query);

View File

@ -66,7 +66,9 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
this.isExecuting(false);
this.close();
this.container.selectedNode(selectedCollection.database);
this.container.closeAllTabsForResource(selectedCollection.rid);
this.container.tabsManager?.closeTabsByComparator(
(tab: ViewModels.Tab) => tab.node && tab.node.rid === selectedCollection.rid
);
this.container.refreshAllDatabases();
this.resetData();
TelemetryProcessor.traceSuccess(

View File

@ -11,6 +11,7 @@ import Explorer from "../Explorer";
import { CollectionStub, DatabaseStub, ExplorerStub } from "../OpenActionsStubs";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { TreeNode } from "../../Contracts/ViewModels";
import { TabsManager } from "../Tabs/TabsManager";
describe("Delete Database Confirmation Pane", () => {
describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => {
@ -123,6 +124,7 @@ describe("Delete Database Confirmation Pane", () => {
);
sinon.stub(fakeExplorer, "documentClientUtility").value(fakeDocumentClientUtility);
sinon.stub(fakeExplorer, "selectedNode").value(ko.observable<TreeNode>());
sinon.stub(fakeExplorer, "tabsManager").value(new TabsManager());
fakeExplorer.isLastNonEmptyDatabase.returns(true);
let pane = new DeleteDatabaseConfirmationPane({

View File

@ -67,11 +67,17 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
this.container.closeAllTabsForResource(selectedDatabase.rid);
this.container.tabsManager.closeTabsByComparator(
(tab: ViewModels.Tab) => tab.node && tab.node.rid === selectedDatabase.rid
);
this.container.selectedNode(null);
selectedDatabase
.collections()
.forEach((collection: ViewModels.Collection) => this.container.closeAllTabsForResource(collection.rid));
.forEach((collection: ViewModels.Collection) =>
this.container.tabsManager.closeTabsByComparator(
(tab: ViewModels.Tab) => tab.node && tab.node.rid === collection.rid
)
);
this.resetData();
TelemetryProcessor.traceSuccess(
Action.DeleteDatabase,

View File

@ -111,7 +111,7 @@ export class LoadQueryPane extends ContextualPaneBase implements ViewModels.Load
const reader = new FileReader();
reader.onload = (evt: any): void => {
const fileData: string = evt.target.result;
const queryTab: ViewModels.QueryTab = this.container.findActiveTab() as ViewModels.QueryTab;
const queryTab: ViewModels.QueryTab = this.container.tabsManager.activeTab() as ViewModels.QueryTab;
queryTab.initialEditorContent(fileData);
queryTab.sqlQueryEditorContent(fileData);
deferred.resolve();

View File

@ -53,7 +53,7 @@ export class RenewAdHocAccessPane extends ContextualPaneBase implements ViewMode
const apiKind: DataModels.ApiKind =
this.container && DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience());
const hasOpenedTabs: boolean =
(this.container && this.container.openedTabs() && this.container.openedTabs().length > 0) || false;
(this.container && this.container.tabsManager && this.container.tabsManager.openedTabs().length > 0) || false;
if (!inputMetadata || inputMetadata.apiKind == null || !inputMetadata.accountName) {
this.formErrors("Invalid connection string input");

View File

@ -34,7 +34,8 @@ export class SaveQueryPane extends ContextualPaneBase {
}
const queryName: string = this.queryName();
const queryTab: ViewModels.QueryTab = this.container && (this.container.findActiveTab() as ViewModels.QueryTab);
const queryTab: ViewModels.QueryTab =
this.container && (this.container.tabsManager.activeTab() as ViewModels.QueryTab);
const query: string = queryTab && queryTab.sqlQueryEditorContent();
if (!queryName || queryName.length === 0) {
this.formErrors("No query name specified");

View File

@ -1,15 +1,16 @@
import * as ko from "knockout";
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import { SplashScreenComponentAdapter } from "./SplashScreenComponentApdapter";
import { TabsManager } from "../Tabs/TabsManager";
import Explorer from "../Explorer";
jest.mock("../Explorer");
const createExplorer = () => {
const mock = new Explorer({} as any);
mock.openedTabs = ko.observableArray([]);
mock.selectedNode = ko.observable();
mock.isNotebookEnabled = ko.observable(false);
mock.addCollectionText = ko.observable("add collection");
mock.tabsManager = new TabsManager();
return mock as jest.Mocked<Explorer>;
};

View File

@ -31,7 +31,7 @@ export class SplashScreenComponentAdapter implements ReactAdapter {
constructor(private container: ViewModels.Explorer) {
this.parameters = ko.observable<number>(Date.now());
this.container.openedTabs.subscribe(tabs => {
this.container.tabsManager.openedTabs.subscribe((tabs: ViewModels.Tab[]) => {
if (tabs.length === 0) {
this.forceRender();
}

View File

@ -21,7 +21,7 @@ import DeleteIcon from "../../../images/delete.svg";
import { QueryIterator, ItemDefinition, Resource, ConflictDefinition } from "@azure/cosmos";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
export class ConflictsTab extends TabsBase implements ViewModels.ConflictsTab {
export default class ConflictsTab extends TabsBase implements ViewModels.ConflictsTab {
public selectedConflictId: ko.Observable<ViewModels.ConflictId>;
public selectedConflictContent: ViewModels.Editable<string>;
public selectedConflictCurrent: ViewModels.Editable<string>;

View File

@ -20,8 +20,7 @@ describe("Documents tab", () => {
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(documentsTab.buildQuery("")).toContain("select");
@ -104,8 +103,7 @@ describe("Documents tab", () => {
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(documentsTab.showPartitionKey).toBe(false);
@ -124,8 +122,7 @@ describe("Documents tab", () => {
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(documentsTab.showPartitionKey).toBe(false);
@ -144,8 +141,7 @@ describe("Documents tab", () => {
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(documentsTab.showPartitionKey).toBe(true);
@ -164,8 +160,7 @@ describe("Documents tab", () => {
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(documentsTab.showPartitionKey).toBe(false);
@ -184,8 +179,7 @@ describe("Documents tab", () => {
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(documentsTab.showPartitionKey).toBe(true);

View File

@ -28,8 +28,7 @@ describe("Query Tab", () => {
selfLink: "",
isActive: ko.observable<boolean>(false),
hashLocation: "",
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
}

View File

@ -76,8 +76,7 @@ describe("Settings tab", () => {
quotaInfo,
null
),
onUpdateTabsButtons: undefined,
openedTabs: []
onUpdateTabsButtons: undefined
});
};
@ -196,8 +195,7 @@ describe("Settings tab", () => {
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(settingsTab.shouldUpdateCollection()).toBe(false);
@ -221,8 +219,7 @@ describe("Settings tab", () => {
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(settingsTab.shouldUpdateCollection()).toBe(false);
@ -241,8 +238,7 @@ describe("Settings tab", () => {
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(settingsTab.shouldUpdateCollection()).toBe(false);
@ -281,8 +277,7 @@ describe("Settings tab", () => {
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
@ -299,8 +294,7 @@ describe("Settings tab", () => {
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
@ -326,8 +320,7 @@ describe("Settings tab", () => {
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
@ -408,8 +401,7 @@ describe("Settings tab", () => {
hashLocation: "",
isActive: ko.observable(false),
collection: getCollection(defaultApi, partitionKeyOption),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
}
@ -555,8 +547,7 @@ describe("Settings tab", () => {
hashLocation: "",
isActive: ko.observable(false),
collection: getCollection(autoPilotTier),
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {},
openedTabs: []
onUpdateTabsButtons: (buttons: ViewModels.NavbarButtonConfig[]): void => {}
});
}
describe("Visible", () => {

View File

@ -252,7 +252,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.Settings
this.throughputModeRadioName = `throughputModeRadio${this.tabId}`;
this.changeFeedPolicyToggled = editable.observable<ChangeFeedPolicyToggledState>(
this.collection.rawDataModel.changeFeedPolicy != null
this.collection.rawDataModel?.changeFeedPolicy != null
? ChangeFeedPolicyToggledState.On
: ChangeFeedPolicyToggledState.Off
);

View File

@ -16,6 +16,7 @@ import TriggerTabTemplate from "./TriggerTab.html";
import UserDefinedFunctionTabTemplate from "./UserDefinedFunctionTab.html";
import GalleryTabTemplate from "./GalleryTab.html";
import NotebookViewerTabTemplate from "./NotebookViewerTab.html";
import TabsManagerTemplate from "./TabsManager.html";
export class TabComponent {
constructor(data: any) {
@ -23,6 +24,15 @@ export class TabComponent {
}
}
export class TabsManager {
constructor() {
return {
viewModel: TabComponent,
template: TabsManagerTemplate
};
}
}
export class DocumentsTab {
constructor() {
return {

View File

@ -24,8 +24,6 @@ export default class TabsBase extends WaitsForTemplateViewModel implements ViewM
public tabKind: ViewModels.CollectionTabKind;
public tabTitle: ko.Observable<string>;
public tabPath: ko.Observable<string>;
public nextTab: ko.Observable<ViewModels.Tab>;
public previousTab: ko.Observable<ViewModels.Tab>;
public closeButtonTabIndex: ko.Computed<number>;
public errorDetailsTabIndex: ko.Computed<number>;
public hashLocation: ko.Observable<string>;
@ -55,8 +53,6 @@ export default class TabsBase extends WaitsForTemplateViewModel implements ViewM
(options.tabPath && ko.observable<string>(options.tabPath)) ||
(this.collection &&
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
this.nextTab = ko.observable<ViewModels.Tab>();
this.previousTab = ko.observable<ViewModels.Tab>();
this.closeButtonTabIndex = ko.computed<number>(() => (this.isActive() ? 0 : null));
this.errorDetailsTabIndex = ko.computed<number>(() => (this.isActive() ? 0 : null));
this.isExecutionError = ko.observable<boolean>(false);
@ -80,34 +76,11 @@ export default class TabsBase extends WaitsForTemplateViewModel implements ViewM
return true;
})
};
const openedTabs = options.openedTabs;
if (openedTabs && openedTabs.length && openedTabs.length > 0) {
const lastTab = openedTabs[openedTabs.length - 1];
lastTab && lastTab.nextTab(this);
this.previousTab(lastTab);
}
}
public onCloseTabButtonClick(): Q.Promise<any> {
const previousTab = this.previousTab();
const nextTab = this.nextTab();
previousTab && previousTab.nextTab(nextTab);
nextTab && nextTab.previousTab(previousTab);
this.getContainer().openedTabs.remove(tab => tab.tabId === this.tabId);
const tabToActivate = nextTab || previousTab;
if (!tabToActivate) {
this.getContainer().selectedNode(null);
this.getContainer().onUpdateTabsButtons([]);
this.getContainer().activeTab(null);
} else {
tabToActivate.isActive(true);
this.getContainer().activeTab(tabToActivate);
}
public onCloseTabButtonClick(): void {
const explorer: ViewModels.Explorer = this.getContainer();
explorer.tabsManager.closeTab(this.tabId, explorer);
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
databaseAccountName: this.getContainer().databaseAccount().name,
@ -115,16 +88,10 @@ export default class TabsBase extends WaitsForTemplateViewModel implements ViewM
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle()
});
return Q();
}
public onTabClick(): Q.Promise<any> {
for (let i = 0; i < this.getContainer().openedTabs().length; i++) {
const tab = this.getContainer().openedTabs()[i];
tab.isActive(false);
}
this.isActive(true);
this.getContainer().activeTab(this);
this.getContainer().tabsManager.activateTab(this);
return Q();
}

View File

@ -0,0 +1,148 @@
<div id="content" class="flexContainer hideOverflows" data-bind="visible: openedTabs && openedTabs().length > 0">
<!-- Tabs - Start -->
<div class="nav-tabs-margin">
<ul class="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
<!-- ko foreach: openedTabs -->
<li
class="tabList"
data-bind="
attr: {
title: $data.tabPath,
'aria-selected': $data.isActive,
'aria-expanded': $data.isActive,
'aria-controls': $data.tabId
},
css:{
active: $data.isActive
},
hasFocus: $data.hasFocus,
event: { keypress: onKeyPressActivate },
click: $data.onTabClick,"
tabindex="0"
role="tab"
>
<span class="tabNavContentContainer">
<a data-toggle="tab" data-bind="attr: { href: '#' + $data.tabId }" tabindex="-1">
<div class="tab_Content">
<span class="statusIconContainer">
<div
class="errorIconContainer"
id="errorStatusIcon"
title="Click to view more details"
role="button"
data-bind="
click: onErrorDetailsClick,
event: { keypress: onErrorDetailsKeyPress },
attr: { tabindex: errorDetailsTabIndex },
css: { actionsEnabled: isActive },
visible: isExecutionError"
>
<span class="errorIcon"></span>
</div>
<img
class="loadingIcon"
title="Loading"
src="/circular_loader_black_16x16.gif"
data-bind="visible: $data.isExecuting"
alt="Loading"
/>
</span>
<span class="tabNavText" data-bind="text: $data.tabTitle"></span>
<span class="tabIconSection">
<span
aria-label="Close Tab"
role="button"
class="cancelButton"
data-bind="
click: $data.onCloseTabButtonClick,
event: { keypress: onKeyPressClose },
attr: { tabindex: $data.closeButtonTabIndex },
visible: $data.isActive() || $data.isMouseOver()"
title="Close"
>
<span class="tabIcon close-Icon" data-bind="visible: $data.isActive() || $data.isMouseOver()">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</span>
</span>
</span>
</div>
</a>
</span>
</li>
<!-- /ko -->
</ul>
</div>
<!-- Tabs -- End -->
<!-- Tabs Panes -- Start -->
<div class="tabPanesContainer">
<!-- ko foreach: openedTabs -->
<div class="tabs-container" data-bind="visible: $data.isActive">
<!-- ko if: $data.tabKind === 0 -->
<documents-tab params="{data: $data}"></documents-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 1 -->
<settings-tab params="{data: $data}"></settings-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 2 -->
<stored-procedure-tab params="{data: $data}"></stored-procedure-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 3 -->
<user-defined-function-tab params="{data: $data}"></user-defined-function-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 4 -->
<trigger-tab params="{data: $data}"></trigger-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 5 -->
<query-tab params="{data: $data}"></query-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 6 -->
<graph-tab params="{data: $data}"></graph-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 9 -->
<tables-query-tab class="flexContainer" params="{data: $data}"></tables-query-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 10 -->
<mongo-shell-tab params="{data: $data}"></mongo-shell-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 11 -->
<database-settings-tab params="{data: $data}"></database-settings-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 12 -->
<conflicts-tab params="{data: $data}"></conflicts-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 14 -->
<terminal-tab params="{data: $data}"></terminal-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 15 -->
<notebookv2-tab params="{data: $data}"></notebookv2-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 16 -->
<spark-master-tab params="{data: $data}"></spark-master-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 17 -->
<gallery-tab params="{data: $data}"></gallery-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 18 -->
<notebook-viewer-tab params="{data: $data}"></notebook-viewer-tab>
<!-- /ko -->
</div>
<!-- /ko -->
</div>
<!-- Tabs Panes - End -->
</div>

View File

@ -0,0 +1,138 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { CollectionStub, DatabaseStub } from "../../Explorer/OpenActionsStubs";
import { DataAccessUtility } from "../../Platform/Portal/DataAccessUtility";
import { TabsManager } from "./TabsManager";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
import DocumentsTab from "./DocumentsTab";
import Explorer from "../Explorer";
import QueryTab from "./QueryTab";
describe("Tabs manager tests", () => {
let tabsManager: TabsManager;
let explorer: ViewModels.Explorer;
let database: ViewModels.Database;
let collection: ViewModels.Collection;
let queryTab: QueryTab;
let documentsTab: DocumentsTab;
beforeAll(() => {
explorer = new Explorer({ documentClientUtility: undefined, notificationsClient: undefined, isEmulator: false });
explorer.databaseAccount = ko.observable<ViewModels.DatabaseAccount>({
id: "test",
name: "test",
location: "",
type: "",
kind: "",
tags: "",
properties: undefined
});
database = new DatabaseStub({
container: explorer,
id: ko.observable<string>("test"),
isDatabaseShared: () => false
});
database.isDatabaseExpanded = ko.observable<boolean>(true);
database.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
collection = new CollectionStub({
container: explorer,
databaseId: "test",
id: ko.observable<string>("test")
});
collection.getDatabase = (): ViewModels.Database => database;
collection.isCollectionExpanded = ko.observable<boolean>(true);
collection.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
queryTab = new QueryTab({
tabKind: ViewModels.CollectionTabKind.Query,
collection,
database,
title: "",
tabPath: "",
documentClientUtility: explorer.documentClientUtility,
selfLink: "",
isActive: ko.observable<boolean>(false),
hashLocation: "",
onUpdateTabsButtons: undefined
});
documentsTab = new DocumentsTab({
partitionKey: undefined,
documentIds: ko.observableArray<ViewModels.DocumentId>(),
tabKind: ViewModels.CollectionTabKind.Documents,
collection,
title: "",
tabPath: "",
documentClientUtility: new DocumentClientUtilityBase(new DataAccessUtility()),
selfLink: "",
hashLocation: "",
isActive: ko.observable<boolean>(false),
onUpdateTabsButtons: undefined
});
});
beforeEach(() => {
tabsManager = new TabsManager();
explorer.tabsManager = tabsManager;
});
it("open new tabs", () => {
tabsManager.activateNewTab(queryTab);
expect(tabsManager.openedTabs().length).toBe(1);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true);
tabsManager.activateNewTab(documentsTab);
expect(tabsManager.openedTabs().length).toBe(2);
expect(tabsManager.openedTabs()[1]).toEqual(documentsTab);
expect(tabsManager.activeTab()).toEqual(documentsTab);
expect(queryTab.isActive()).toBe(false);
expect(documentsTab.isActive()).toBe(true);
});
it("open existing tabs", () => {
tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab);
tabsManager.activateTab(queryTab);
expect(tabsManager.openedTabs().length).toBe(2);
expect(tabsManager.activeTab()).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true);
expect(documentsTab.isActive()).toBe(false);
});
it("get tabs", () => {
tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab);
const queryTabs: ViewModels.Tab[] = tabsManager.getTabs(ViewModels.CollectionTabKind.Query);
expect(queryTabs.length).toBe(1);
expect(queryTabs[0]).toEqual(queryTab);
const documentsTabs: ViewModels.Tab[] = tabsManager.getTabs(
ViewModels.CollectionTabKind.Documents,
(tab: ViewModels.Tab) => tab.tabId === documentsTab.tabId
);
expect(documentsTabs.length).toBe(1);
expect(documentsTabs[0]).toEqual(documentsTab);
});
it("close tabs", () => {
tabsManager.activateNewTab(queryTab);
tabsManager.activateNewTab(documentsTab);
tabsManager.closeTab(documentsTab.tabId, explorer);
expect(tabsManager.openedTabs().length).toBe(1);
expect(tabsManager.openedTabs()[0]).toEqual(queryTab);
expect(tabsManager.activeTab()).toEqual(queryTab);
expect(queryTab.isActive()).toBe(true);
expect(documentsTab.isActive()).toBe(false);
tabsManager.closeTabsByComparator((tab: ViewModels.Tab) => tab.tabId === queryTab.tabId);
expect(tabsManager.openedTabs().length).toBe(0);
expect(tabsManager.activeTab()).toEqual(undefined);
expect(queryTab.isActive()).toBe(false);
});
});

View File

@ -0,0 +1,96 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import TabsManagerTemplate from "./TabsManager.html";
export class TabsManager {
public openedTabs: ko.ObservableArray<ViewModels.Tab>;
public activeTab: ko.Observable<ViewModels.Tab>;
constructor() {
this.openedTabs = ko.observableArray<ViewModels.Tab>([]);
this.activeTab = ko.observable<ViewModels.Tab>();
}
public activateNewTab(tab: ViewModels.Tab): void {
this.openedTabs.push(tab);
this.activateTab(tab);
}
public activateTab(tab: ViewModels.Tab): void {
this.activeTab() && this.activeTab().isActive(false);
tab.isActive(true);
this.activeTab(tab);
}
public getTabs(
tabKind: ViewModels.CollectionTabKind,
comparator?: (tab: ViewModels.Tab) => boolean
): ViewModels.Tab[] {
return this.openedTabs().filter((openedTab: ViewModels.Tab) => {
return openedTab.tabKind === tabKind && (!comparator || comparator(openedTab));
});
}
public refreshActiveTab(comparator: (tab: ViewModels.Tab) => boolean): void {
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state
this.openedTabs().forEach((tab: ViewModels.Tab) => {
if (comparator(tab) && tab.isActive()) {
tab.onActivate();
}
});
}
public removeTabById(tabId: string): void {
this.openedTabs.remove((tab: ViewModels.Tab) => tab.tabId === tabId);
}
public removeTabByComparator(comparator: (tab: ViewModels.Tab) => boolean): void {
this.openedTabs.remove((tab: ViewModels.Tab) => comparator(tab));
}
public closeTabsByComparator(comparator: (tab: ViewModels.Tab) => boolean): void {
this.activeTab() && this.activeTab().isActive(false);
this.activeTab(undefined);
this.openedTabs().forEach((tab: ViewModels.Tab) => {
if (comparator(tab)) {
tab.onCloseTabButtonClick();
}
});
}
public closeTabs(): void {
this.openedTabs([]);
}
public closeTab(tabId: string, explorer: ViewModels.Explorer): void {
const tabIndex: number = this.openedTabs().findIndex((tab: ViewModels.Tab) => tab.tabId === tabId);
if (tabIndex !== -1) {
const tabToActive: ViewModels.Tab = this.openedTabs()[tabIndex + 1] || this.openedTabs()[tabIndex - 1];
this.openedTabs()[tabIndex].isActive(false);
this.removeTabById(tabId);
if (tabToActive) {
tabToActive.isActive(true);
this.activeTab(tabToActive);
} else {
explorer.selectedNode(undefined);
explorer.onUpdateTabsButtons([]);
this.activeTab(undefined);
}
}
}
public isTabActive(tabKind: ViewModels.CollectionTabKind): boolean {
return this.activeTab() && this.activeTab().tabKind === tabKind;
}
}
function TabsManagerWrapperViewModel(params: { data: TabsManager }) {
return params.data;
}
export function TabsManagerKOComponent(): unknown {
return {
viewModel: TabsManagerWrapperViewModel,
template: TabsManagerTemplate
};
}

View File

@ -16,7 +16,10 @@ import { OfferUtils } from "../../Utils/OfferUtils";
import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
import { ConflictsTab } from "../Tabs/ConflictsTab";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentsTab from "../Tabs/DocumentsTab";
import GraphTab from "../Tabs/GraphTab";
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
@ -25,8 +28,6 @@ import MongoShellTab from "../Tabs/MongoShellTab";
import QueryTab from "../Tabs/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab";
import SettingsTab from "../Tabs/SettingsTab";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
@ -229,7 +230,9 @@ export default class Collection implements ViewModels.Collection {
this.expandCollection();
}
this.container.onUpdateTabsButtons([]);
this.refreshActiveTab();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
);
}
public collapseCollection() {
@ -278,14 +281,15 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree
});
// create documents tab if not created yet
const openedTabs = this.container.openedTabs();
const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.Documents,
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
) as DocumentsTab[];
let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0];
let documentsTab: ViewModels.Tab = openedTabs
.filter(tab => tab.collection && tab.collection.rid === this.rid)
.filter(tab => tab.tabKind === ViewModels.CollectionTabKind.Documents)[0];
if (!documentsTab) {
if (documentsTab) {
this.container.tabsManager.activateTab(documentsTab);
} else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.databaseId,
@ -295,6 +299,7 @@ export default class Collection implements ViewModels.Collection {
tabTitle: "Items"
});
this.documentIds([]);
documentsTab = new DocumentsTab({
partitionKey: this.partitionKey,
documentIds: ko.observableArray<DocumentId>([]),
@ -309,14 +314,11 @@ export default class Collection implements ViewModels.Collection {
tabPath: `${this.databaseId}>${this.id()}>Documents`,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/documents`,
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(documentsTab);
}
// Activate
documentsTab.onTabClick();
this.container.tabsManager.activateNewTab(documentsTab);
}
}
public onConflictsClick() {
@ -331,14 +333,15 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree
});
// create documents tab if not created yet
const openedTabs = this.container.openedTabs();
const conflictsTabs: ConflictsTab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.Conflicts,
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
) as ConflictsTab[];
let conflictsTab: ConflictsTab = conflictsTabs && conflictsTabs[0];
let conflictsTab: ViewModels.Tab = openedTabs
.filter(tab => tab.collection && tab.collection.rid === this.rid)
.filter(tab => tab.tabKind === ViewModels.CollectionTabKind.Conflicts)[0];
if (!conflictsTab) {
if (conflictsTab) {
this.container.tabsManager.activateTab(conflictsTab);
} else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.databaseId,
@ -348,7 +351,8 @@ export default class Collection implements ViewModels.Collection {
tabTitle: "Conflicts"
});
this.documentIds([]);
conflictsTab = new ConflictsTab({
const conflictsTab: ConflictsTab = new ConflictsTab({
partitionKey: this.partitionKey,
conflictIds: ko.observableArray<ConflictId>([]),
tabKind: ViewModels.CollectionTabKind.Conflicts,
@ -362,14 +366,11 @@ export default class Collection implements ViewModels.Collection {
tabPath: `${this.databaseId}>${this.id()}>Conflicts`,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/conflicts`,
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(conflictsTab);
}
// Activate
conflictsTab.onTabClick();
this.container.tabsManager.activateNewTab(conflictsTab);
}
}
public onTableEntitiesClick() {
@ -390,14 +391,15 @@ export default class Collection implements ViewModels.Collection {
});
}
// create entities tab if not created yet
const openedTabs = this.container.openedTabs();
const queryTablesTabs: QueryTablesTab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.QueryTables,
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
) as QueryTablesTab[];
let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0];
let documentsTab: ViewModels.Tab = openedTabs
.filter(tab => tab.collection && tab.collection.rid === this.rid)
.filter(tab => tab.tabKind === ViewModels.CollectionTabKind.QueryTables)[0];
if (!documentsTab) {
if (queryTablesTab) {
this.container.tabsManager.activateTab(queryTablesTab);
} else {
this.documentIds([]);
let title = `Entities`;
if (this.container.isPreferredApiCassandra()) {
@ -411,7 +413,8 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: title
});
documentsTab = new QueryTablesTab({
queryTablesTab = new QueryTablesTab({
tabKind: ViewModels.CollectionTabKind.QueryTables,
title: title,
tabPath: "",
@ -423,14 +426,11 @@ export default class Collection implements ViewModels.Collection {
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/entities`,
isActive: ko.observable(false),
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(documentsTab);
}
// Activate
documentsTab.onTabClick();
this.container.tabsManager.activateNewTab(queryTablesTab);
}
}
public onGraphDocumentsClick() {
@ -445,24 +445,17 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree
});
// create documents tab if not created yet
const openedTabs = this.container.openedTabs();
const graphTabs: GraphTab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.Graph,
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
) as GraphTab[];
let graphTab: GraphTab = graphTabs && graphTabs[0];
let documentsTab: ViewModels.Tab = openedTabs
.filter(tab => tab.collection && tab.collection.rid === this.rid)
.filter(tab => tab.tabKind === ViewModels.CollectionTabKind.Graph)[0];
if (!documentsTab) {
if (graphTab) {
this.container.tabsManager.activateTab(graphTab);
} else {
this.documentIds([]);
documentsTab = this._createGraphTab("Graph");
this.container.openedTabs.push(documentsTab);
}
// Activate
documentsTab.onTabClick();
}
private _createGraphTab = (title: string): ViewModels.GraphTab => {
const title = "Graph";
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.databaseId,
@ -471,8 +464,8 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: title
});
// TODO where does the success/failure trace for this tab go?
return new GraphTab({
graphTab = new GraphTab({
account: CosmosClient.databaseAccount(),
tabKind: ViewModels.CollectionTabKind.Graph,
node: this,
@ -489,10 +482,12 @@ export default class Collection implements ViewModels.Collection {
databaseId: this.databaseId,
isTabsContentExpanded: this.container.isTabsContentExpanded,
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
};
this.container.tabsManager.activateNewTab(graphTab);
}
}
public onMongoDBDocumentsClick = () => {
this.container.selectedNode(this);
@ -506,14 +501,15 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree
});
// create documents tab if not created yet
const openedTabs = this.container.openedTabs();
const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.Documents,
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
) as MongoDocumentsTab[];
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0];
let documentsTab: ViewModels.Tab = openedTabs
.filter(tab => tab.collection && tab.collection && tab.collection.rid === this.rid)
.filter(tab => tab.tabKind === ViewModels.CollectionTabKind.Documents)[0];
if (!documentsTab) {
if (mongoDocumentsTab) {
this.container.tabsManager.activateTab(mongoDocumentsTab);
} else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.databaseId,
@ -523,7 +519,8 @@ export default class Collection implements ViewModels.Collection {
tabTitle: "Documents"
});
this.documentIds([]);
documentsTab = new MongoDocumentsTab({
mongoDocumentsTab = new MongoDocumentsTab({
partitionKey: this.partitionKey,
documentIds: this.documentIds,
tabKind: ViewModels.CollectionTabKind.Documents,
@ -537,14 +534,10 @@ export default class Collection implements ViewModels.Collection {
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoDocuments`,
isActive: ko.observable(false),
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(documentsTab);
this.container.tabsManager.activateNewTab(mongoDocumentsTab);
}
// Activate
documentsTab.onTabClick();
};
public onSettingsClick = () => {
@ -559,14 +552,15 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree
});
// create settings tab if not created yet
const openedTabs = this.container.openedTabs();
let settingsTab: ViewModels.Tab = openedTabs
.filter(tab => tab.collection && tab.collection.rid === this.rid)
.filter(tab => tab.tabKind === ViewModels.CollectionTabKind.Settings)[0];
const tabTitle = "Scale & Settings";
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
const matchingTabs: ViewModels.Tab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.Settings,
(tab: ViewModels.Tab) => {
return tab.collection && tab.collection.rid === this.rid;
}
);
let settingsTab: SettingsTab = matchingTabs && (matchingTabs[0] as SettingsTab);
if (!settingsTab) {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount().name,
@ -592,12 +586,10 @@ export default class Collection implements ViewModels.Collection {
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/settings`,
isActive: ko.observable(false),
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
(settingsTab as ViewModels.SettingsTab).pendingNotification(pendingNotification);
this.container.openedTabs.push(settingsTab);
settingsTab.onTabClick(); // Activate
this.container.tabsManager.activateNewTab(settingsTab);
settingsTab.pendingNotification(pendingNotification);
},
(error: any) => {
TelemetryProcessor.traceFailure(
@ -623,12 +615,12 @@ export default class Collection implements ViewModels.Collection {
} else {
pendingNotificationsPromise.then(
(pendingNotification: DataModels.Notification) => {
(settingsTab as ViewModels.SettingsTab).pendingNotification(pendingNotification);
settingsTab.onTabClick();
settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateTab(settingsTab);
},
(error: any) => {
(settingsTab as ViewModels.SettingsTab).pendingNotification(undefined);
settingsTab.onTabClick();
settingsTab.pendingNotification(undefined);
this.container.tabsManager.activateTab(settingsTab);
}
);
}
@ -738,9 +730,7 @@ export default class Collection implements ViewModels.Collection {
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source;
const explorer: ViewModels.Explorer = source.container;
const openedTabs = explorer.openedTabs();
const id = openedTabs.filter(t => t.tabKind === ViewModels.CollectionTabKind.Query).length + 1;
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
const title = "Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount().name,
@ -751,7 +741,7 @@ export default class Collection implements ViewModels.Collection {
tabTitle: title
});
let queryTab: ViewModels.Tab = new QueryTab({
const queryTab: QueryTab = new QueryTab({
tabKind: ViewModels.CollectionTabKind.Query,
title: title,
tabPath: "",
@ -764,20 +754,15 @@ export default class Collection implements ViewModels.Collection {
queryText: queryText,
partitionKey: collection.partitionKey,
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(queryTab);
// Activate
queryTab.onTabClick();
this.container.tabsManager.activateNewTab(queryTab);
}
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source;
const explorer: ViewModels.Explorer = source.container;
const openedTabs = explorer.openedTabs();
const id = openedTabs.filter(t => t.tabKind === ViewModels.CollectionTabKind.Query).length + 1;
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
const title = "Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
@ -789,7 +774,7 @@ export default class Collection implements ViewModels.Collection {
tabTitle: title
});
let queryTab: ViewModels.Tab = new MongoQueryTab({
const mongoQueryTab: MongoQueryTab = new MongoQueryTab({
tabKind: ViewModels.CollectionTabKind.Query,
title: title,
tabPath: "",
@ -801,26 +786,51 @@ export default class Collection implements ViewModels.Collection {
isActive: ko.observable(false),
partitionKey: collection.partitionKey,
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(queryTab);
// Activate
queryTab.onTabClick();
this.container.tabsManager.activateNewTab(mongoQueryTab);
}
public onNewGraphClick() {
var id = this.container.openedTabs().filter(t => t.tabKind === ViewModels.CollectionTabKind.Graph).length + 1;
var graphTab = this._createGraphTab("Graph Query " + id);
this.container.openedTabs.push(graphTab);
// Activate
graphTab.onTabClick();
const id: number = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Graph).length + 1;
const title: string = "Graph Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.databaseId,
collectionName: this.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: title
});
const graphTab: GraphTab = new GraphTab({
account: CosmosClient.databaseAccount(),
tabKind: ViewModels.CollectionTabKind.Graph,
node: this,
title: title,
tabPath: "",
documentClientUtility: this.container.documentClientUtility,
collection: this,
selfLink: this.self,
masterKey: CosmosClient.masterKey() || "",
collectionPartitionKeyProperty: this.partitionKeyProperty,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
collectionId: this.id(),
isActive: ko.observable(false),
databaseId: this.databaseId,
isTabsContentExpanded: this.container.isTabsContentExpanded,
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.tabsManager.activateNewTab(graphTab);
}
public onNewMongoShellClick() {
var id = this.container.openedTabs().filter(t => t.tabKind === ViewModels.CollectionTabKind.MongoShell).length + 1;
var mongoShellTab = new MongoShellTab({
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.MongoShell).length + 1;
const mongoShellTab: MongoShellTab = new MongoShellTab({
tabKind: ViewModels.CollectionTabKind.MongoShell,
title: "Shell " + id,
tabPath: "",
@ -830,14 +840,10 @@ export default class Collection implements ViewModels.Collection {
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoShell`,
selfLink: this.self,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(mongoShellTab);
// Activate
mongoShellTab.onTabClick();
this.container.tabsManager.activateNewTab(mongoShellTab);
}
public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) {
@ -898,7 +904,9 @@ export default class Collection implements ViewModels.Collection {
} else {
this.expandStoredProcedures();
}
this.refreshActiveTab();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
);
}
public expandStoredProcedures() {
@ -955,7 +963,9 @@ export default class Collection implements ViewModels.Collection {
} else {
this.expandUserDefinedFunctions();
}
this.refreshActiveTab();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
);
}
public expandUserDefinedFunctions() {
@ -1012,7 +1022,9 @@ export default class Collection implements ViewModels.Collection {
} else {
this.expandTriggers();
}
this.refreshActiveTab();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
);
}
public expandTriggers() {
@ -1366,19 +1378,6 @@ export default class Collection implements ViewModels.Collection {
});
}
public refreshActiveTab(): void {
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state
const openedRelevantTabs: ViewModels.Tab[] = this.container
.openedTabs()
.filter((tab: ViewModels.Tab) => tab && tab.collection && tab.collection.rid === this.rid);
openedRelevantTabs.forEach((tab: ViewModels.Tab) => {
if (tab.isActive()) {
tab.onActivate();
}
});
}
protected _getOfferForCollection(offers: DataModels.Offer[], collection: DataModels.Collection): DataModels.Offer {
return _.find(offers, (offer: DataModels.Offer) => offer.resource.indexOf(collection._rid) >= 0);
}

View File

@ -49,12 +49,12 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.ResourceTree
});
// create settings tab if not created yet
const openedTabs = this.container.openedTabs();
let settingsTab: ViewModels.Tab = openedTabs
.filter(tab => tab.rid === this.rid)
.filter(tab => tab.tabKind === ViewModels.CollectionTabKind.DatabaseSettings)[0];
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
const matchingTabs: ViewModels.Tab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.DatabaseSettings,
(tab: ViewModels.Tab) => tab.rid === this.rid
);
let settingsTab: DatabaseSettingsTab = matchingTabs && (matchingTabs[0] as DatabaseSettingsTab);
if (!settingsTab) {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount().name,
@ -78,12 +78,11 @@ export default class Database implements ViewModels.Database {
selfLink: this.self,
isActive: ko.observable(false),
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
(settingsTab as ViewModels.DatabaseSettingsTab).pendingNotification(pendingNotification);
this.container.openedTabs.push(settingsTab);
settingsTab.onTabClick(); // Activate
settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateNewTab(settingsTab);
},
(error: any) => {
TelemetryProcessor.traceFailure(
@ -109,12 +108,12 @@ export default class Database implements ViewModels.Database {
} else {
pendingNotificationsPromise.then(
(pendingNotification: DataModels.Notification) => {
(settingsTab as ViewModels.DatabaseSettingsTab).pendingNotification(pendingNotification);
settingsTab.onTabClick();
settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateTab(settingsTab);
},
(error: any) => {
(settingsTab as ViewModels.DatabaseSettingsTab).pendingNotification(undefined);
settingsTab.onTabClick();
settingsTab.pendingNotification(undefined);
this.container.tabsManager.activateTab(settingsTab);
}
);
}
@ -221,7 +220,9 @@ export default class Database implements ViewModels.Database {
this.expandDatabase();
}
this.container.onUpdateTabsButtons([]);
this.refreshTabSelectedState();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.getDatabase().rid === this.rid
);
}
public expandDatabase() {
@ -286,18 +287,6 @@ export default class Database implements ViewModels.Database {
database.container.addCollectionPane.open();
}
public refreshTabSelectedState(): void {
const openedRelevantTabs: ViewModels.Tab[] = this.container
.openedTabs()
.filter((tab: ViewModels.Tab) => tab && tab.collection && tab.collection.getDatabase().rid === this.rid);
openedRelevantTabs.forEach((tab: ViewModels.Tab) => {
if (tab.isActive()) {
tab.onTabClick(); // this ensures the next (deepest) item in the resource tree is highlighted
}
});
}
public findCollectionWithId(collectionId: string): ViewModels.Collection {
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId);
}

View File

@ -73,24 +73,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
});
}
public refreshActiveTab(): void {
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state
const openedRelevantTabs: ViewModels.Tab[] = this.container
.openedTabs()
.filter((tab: ViewModels.Tab) => tab && tab.collection && tab.collection.rid === this.rid);
openedRelevantTabs.forEach((tab: ViewModels.Tab) => {
if (tab.isActive()) {
tab.onActivate();
}
});
}
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source;
const explorer: ViewModels.Explorer = source.container;
const openedTabs = explorer.openedTabs();
const id = openedTabs.filter(t => t.tabKind === ViewModels.CollectionTabKind.Query).length + 1;
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
const title = "Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount().name,
@ -101,7 +86,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
tabTitle: title
});
let queryTab: ViewModels.Tab = new QueryTab({
const queryTab: QueryTab = new QueryTab({
tabKind: ViewModels.CollectionTabKind.Query,
title: title,
tabPath: "",
@ -115,13 +100,10 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
partitionKey: collection.partitionKey,
resourceTokenPartitionKey: this.container.resourceTokenPartitionKey(),
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(queryTab);
// Activate
queryTab.onTabClick();
this.container.tabsManager.activateNewTab(queryTab);
}
public onDocumentDBDocumentsClick() {
@ -136,14 +118,15 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
dataExplorerArea: Constants.Areas.ResourceTree
});
// create documents tab if not created yet
const openedTabs = this.container.openedTabs();
const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.Documents,
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
) as DocumentsTab[];
let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0];
let documentsTab: ViewModels.Tab = openedTabs
.filter(tab => tab.collection && tab.collection.rid === this.rid)
.filter(tab => tab.tabKind === ViewModels.CollectionTabKind.Documents)[0];
if (!documentsTab) {
if (documentsTab) {
this.container.tabsManager.activateTab(documentsTab);
} else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
databaseName: this.databaseId,
@ -152,6 +135,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Items"
});
documentsTab = new DocumentsTab({
partitionKey: this.partitionKey,
resourceTokenPartitionKey: this.container.resourceTokenPartitionKey(),
@ -167,14 +151,11 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
tabPath: `${this.databaseId}>${this.id()}>Documents`,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/documents`,
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(documentsTab);
}
// Activate
documentsTab.onTabClick();
this.container.tabsManager.activateNewTab(documentsTab);
}
}
public getDatabase(): ViewModels.Database {

View File

@ -46,7 +46,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.parameters = ko.observable(Date.now());
this.container.selectedNode.subscribe((newValue: any) => this.triggerRender());
this.container.activeTab.subscribe((newValue: ViewModels.Tab) => this.triggerRender());
this.container.tabsManager.activeTab.subscribe((newValue: ViewModels.Tab) => this.triggerRender());
this.container.isNotebookEnabled.subscribe(newValue => this.triggerRender());
this.koSubsDatabaseIdMap = new ArrayHashMap();
@ -171,7 +171,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
}
database.selectDatabase();
this.container.onUpdateTabsButtons([]);
database.refreshTabSelectedState();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.getDatabase().rid === database.rid
);
},
onContextMenuOpen: () => this.container.selectedNode(database)
};
@ -268,7 +270,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
// Rewritten version of expandCollapseCollection
this.container.selectedNode(collection);
this.container.onUpdateTabsButtons([]);
collection.refreshActiveTab();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === collection.rid
);
},
onExpanded: () => {
if (ResourceTreeAdapter.showScriptNodes(this.container)) {
@ -294,7 +298,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
collection.refreshActiveTab();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === collection.rid
);
}
};
}
@ -311,7 +317,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
collection.refreshActiveTab();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === collection.rid
);
}
};
}
@ -327,7 +335,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
})),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
collection.refreshActiveTab();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === collection.rid
);
}
};
}
@ -409,7 +419,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
className: "notebookHeader galleryHeader",
onClick: () => this.container.openGallery(),
isSelected: () => {
const activeTab = this.container.findActiveTab();
const activeTab = this.container.tabsManager.activeTab();
return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery;
}
};
@ -517,7 +527,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
className: "notebookHeader",
onClick: () => onFileClick(item),
isSelected: () => {
const activeTab = this.container.findActiveTab();
const activeTab = this.container.tabsManager.activeTab();
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
@ -634,7 +644,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
}
},
isSelected: () => {
const activeTab = this.container.findActiveTab();
const activeTab = this.container.tabsManager.activeTab();
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
@ -657,14 +667,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
private getActiveTab(): ViewModels.Tab {
const activeTabs: ViewModels.Tab[] = this.container.openedTabs().filter((tab: ViewModels.Tab) => tab.isActive());
if (activeTabs.length) {
return activeTabs[0];
}
return undefined;
}
private isDataNodeSelected(rid: string, nodeKind: string, subnodeKind: ViewModels.CollectionTabKind): boolean {
if (!this.container.selectedNode || !this.container.selectedNode()) {
return false;
@ -674,7 +676,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
if (subnodeKind === undefined) {
return selectedNode.rid === rid && selectedNode.nodeKind === nodeKind;
} else {
const activeTab = this.getActiveTab();
const activeTab = this.container.tabsManager.activeTab();
let selectedSubnodeKind;
if (nodeKind === "Database" && (selectedNode as ViewModels.Database).selectedSubnodeKind) {
selectedSubnodeKind = (selectedNode as ViewModels.Database).selectedSubnodeKind();

View File

@ -12,9 +12,7 @@ const createMockContainer = (): ViewModels.Explorer => {
let mockContainer = {} as ViewModels.Explorer;
mockContainer.resourceTokenCollection = createMockCollection(mockContainer);
mockContainer.selectedNode = ko.observable<ViewModels.TreeNode>();
mockContainer.activeTab = ko.observable<ViewModels.Tab>();
mockContainer.mostRecentActivity = new MostRecentActivity.MostRecentActivity(mockContainer);
mockContainer.openedTabs = ko.observableArray<ViewModels.Tab>([]);
mockContainer.onUpdateTabsButtons = () => {};
return mockContainer;

View File

@ -17,7 +17,8 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
this.container.resourceTokenCollection.subscribe((collection: ViewModels.CollectionBase) => this.triggerRender());
this.container.selectedNode.subscribe((newValue: any) => this.triggerRender());
this.container.activeTab.subscribe((newValue: ViewModels.Tab) => this.triggerRender());
this.container.tabsManager &&
this.container.tabsManager.activeTab.subscribe((newValue: ViewModels.Tab) => this.triggerRender());
this.triggerRender();
}
@ -63,7 +64,9 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
// Rewritten version of expandCollapseCollection
this.container.selectedNode(collection);
this.container.onUpdateTabsButtons([]);
collection.refreshActiveTab();
this.container.tabsManager.refreshActiveTab(
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === collection.rid
);
},
isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", undefined)
};
@ -75,14 +78,6 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
};
}
private getActiveTab(): ViewModels.Tab {
const activeTabs: ViewModels.Tab[] = this.container.openedTabs().filter((tab: ViewModels.Tab) => tab.isActive());
if (activeTabs.length) {
return activeTabs[0];
}
return undefined;
}
private isDataNodeSelected(rid: string, nodeKind: string, subnodeKind: ViewModels.CollectionTabKind): boolean {
if (!this.container.selectedNode || !this.container.selectedNode()) {
return false;
@ -92,7 +87,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
if (subnodeKind) {
return selectedNode.rid === rid && selectedNode.nodeKind === nodeKind;
} else {
const activeTab = this.getActiveTab();
const activeTab = this.container.tabsManager.activeTab();
let selectedSubnodeKind;
if (nodeKind === "Database" && (selectedNode as ViewModels.Database).selectedSubnodeKind) {
selectedSubnodeKind = (selectedNode as ViewModels.Database).selectedSubnodeKind();

View File

@ -56,15 +56,13 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
}
public static create(source: ViewModels.Collection, event: MouseEvent) {
const openedTabs = source.container.openedTabs();
const id =
openedTabs.filter((tab: ViewModels.Tab) => tab.tabKind === ViewModels.CollectionTabKind.StoredProcedures).length +
1;
const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.StoredProcedures).length + 1;
const storedProcedure = <DataModels.StoredProcedure>{
id: "",
body: sampleStoredProcedureBody
};
let storedProcedureTab: ViewModels.Tab = new StoredProcedureTab({
const storedProcedureTab: StoredProcedureTab = new StoredProcedureTab({
resource: storedProcedure,
isNew: true,
tabKind: ViewModels.CollectionTabKind.StoredProcedures,
@ -76,13 +74,10 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/sproc`,
selfLink: "",
isActive: ko.observable(false),
onUpdateTabsButtons: source.container.onUpdateTabsButtons,
openedTabs: source.container.openedTabs()
onUpdateTabsButtons: source.container.onUpdateTabsButtons
});
source.container.openedTabs.push(storedProcedureTab);
// Activate
storedProcedureTab.onTabClick();
source.container.tabsManager.activateNewTab(storedProcedureTab);
}
public select() {
@ -98,15 +93,15 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
public open = () => {
this.select();
const openedTabs = this.container.openedTabs();
const storedProcedureTabsOpen: ViewModels.Tab[] =
openedTabs &&
openedTabs.filter(
tab => tab.node && tab.node.rid === this.rid && tab.tabKind === ViewModels.CollectionTabKind.StoredProcedures
);
let storedProcedureTab: ViewModels.Tab =
storedProcedureTabsOpen && storedProcedureTabsOpen.length > 0 && storedProcedureTabsOpen[0];
if (!storedProcedureTab) {
const storedProcedureTabs: StoredProcedureTab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.StoredProcedures,
(tab: ViewModels.Tab) => tab.node && tab.node.rid === this.rid
) as StoredProcedureTab[];
let storedProcedureTab: StoredProcedureTab = storedProcedureTabs && storedProcedureTabs[0];
if (storedProcedureTab) {
this.container.tabsManager.activateTab(storedProcedureTab);
} else {
const storedProcedureData = <DataModels.StoredProcedure>{
_rid: this.rid,
_self: this.self,
@ -129,14 +124,11 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
)}/sprocs/${this.id()}`,
selfLink: this.self,
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(storedProcedureTab);
}
// Activate
storedProcedureTab.onTabClick();
this.container.tabsManager.activateNewTab(storedProcedureTab);
}
};
public delete() {
@ -153,7 +145,9 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
this.container.documentClientUtility.deleteStoredProcedure(this.collection, storedProcedureData).then(
() => {
this.container.openedTabs.remove((tab: ViewModels.Tab) => tab.node && tab.node.rid === this.rid);
this.container.tabsManager.removeTabByComparator(
(tab: ViewModels.Tab) => tab.node && tab.node.rid === this.rid
);
this.collection.children.remove(this);
},
reason => {}
@ -161,7 +155,11 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
}
public execute(params: string[], partitionKeyValue?: string): void {
const sprocTab: ViewModels.StoredProcedureTab = this._getCurrentStoredProcedureTab();
const sprocTabs: ViewModels.StoredProcedureTab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.StoredProcedures,
(tab: ViewModels.Tab) => tab.node && tab.node.rid === this.rid
) as ViewModels.StoredProcedureTab[];
const sprocTab: ViewModels.StoredProcedureTab = sprocTabs && sprocTabs.length > 0 && sprocTabs[0];
sprocTab.isExecuting(true);
this.container &&
this.container.documentClientUtility
@ -184,17 +182,4 @@ export default class StoredProcedure implements ViewModels.StoredProcedure {
const focusElement = document.getElementById("execute-storedproc-toggles");
focusElement && focusElement.focus();
}
private _getCurrentStoredProcedureTab(): ViewModels.StoredProcedureTab {
const openedTabs = this.container.openedTabs();
const storedProcedureTabsOpen: ViewModels.Tab[] =
openedTabs &&
openedTabs.filter(
tab => tab.node && tab.node.rid === this.rid && tab.tabKind === ViewModels.CollectionTabKind.StoredProcedures
);
return (storedProcedureTabsOpen &&
storedProcedureTabsOpen.length > 0 &&
storedProcedureTabsOpen[0]) as ViewModels.StoredProcedureTab;
}
}

View File

@ -3,7 +3,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import Collection from "./Collection";
import TriggerTab from "../Tabs/TriggerTab";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@ -41,10 +40,7 @@ export default class Trigger implements ViewModels.Trigger {
}
public static create(source: ViewModels.Collection, event: MouseEvent) {
const id =
source.container
.openedTabs()
.filter((tab: ViewModels.Tab) => tab.tabKind === ViewModels.CollectionTabKind.Triggers).length + 1;
const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Triggers).length + 1;
const trigger = <DataModels.Trigger>{
id: "",
body: "function trigger(){}",
@ -52,7 +48,7 @@ export default class Trigger implements ViewModels.Trigger {
triggerType: "Pre"
};
let triggerTab: ViewModels.Tab = new TriggerTab({
const triggerTab: TriggerTab = new TriggerTab({
resource: trigger,
isNew: true,
tabKind: ViewModels.CollectionTabKind.Triggers,
@ -64,23 +60,24 @@ export default class Trigger implements ViewModels.Trigger {
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/trigger`,
selfLink: "",
isActive: ko.observable(false),
onUpdateTabsButtons: source.container.onUpdateTabsButtons,
openedTabs: source.container.openedTabs()
onUpdateTabsButtons: source.container.onUpdateTabsButtons
});
source.container.openedTabs.push(triggerTab);
// Activate
triggerTab.onTabClick();
source.container.tabsManager.activateNewTab(triggerTab);
}
public open = () => {
this.select();
let triggerTab: ViewModels.Tab = this.container
.openedTabs()
.filter(tab => tab.node && tab.node.rid === this.rid)[0];
if (!triggerTab) {
const triggerTabs: TriggerTab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.Triggers,
(tab: ViewModels.Tab) => tab.node && tab.node.rid === this.rid
) as TriggerTab[];
let triggerTab: TriggerTab = triggerTabs && triggerTabs[0];
if (triggerTab) {
this.container.tabsManager.activateTab(triggerTab);
} else {
const triggerData = <DataModels.Trigger>{
_rid: this.rid,
_self: this.self,
@ -105,15 +102,11 @@ export default class Trigger implements ViewModels.Trigger {
)}/triggers/${this.id()}`,
selfLink: "",
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(triggerTab);
this.container.tabsManager.activateNewTab(triggerTab);
}
// Activate
triggerTab.onTabClick();
};
public delete() {
@ -132,7 +125,9 @@ export default class Trigger implements ViewModels.Trigger {
this.container.documentClientUtility.deleteTrigger(this.collection, triggerData).then(
() => {
this.container.openedTabs.remove((tab: ViewModels.Tab) => tab.node && tab.node.rid === this.rid);
this.container.tabsManager.removeTabByComparator(
(tab: ViewModels.Tab) => tab.node && tab.node.rid === this.rid
);
this.collection.children.remove(this);
},
reason => {}

View File

@ -5,7 +5,6 @@ import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Collection from "./Collection";
export default class UserDefinedFunction implements ViewModels.UserDefinedFunction {
public nodeKind: string;
@ -28,15 +27,13 @@ export default class UserDefinedFunction implements ViewModels.UserDefinedFuncti
}
public static create(source: ViewModels.Collection, event: MouseEvent) {
const id =
source.container
.openedTabs()
.filter((tab: ViewModels.Tab) => tab.tabKind === ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1;
const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1;
const userDefinedFunction = <DataModels.UserDefinedFunction>{
id: "",
body: "function userDefinedFunction(){}"
};
let userDefinedFunctionTab: ViewModels.Tab = new UserDefinedFunctionTab({
const userDefinedFunctionTab: UserDefinedFunctionTab = new UserDefinedFunctionTab({
resource: userDefinedFunction,
isNew: true,
tabKind: ViewModels.CollectionTabKind.UserDefinedFunctions,
@ -48,22 +45,24 @@ export default class UserDefinedFunction implements ViewModels.UserDefinedFuncti
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(source.databaseId, source.id())}/udf`,
selfLink: "",
isActive: ko.observable(false),
onUpdateTabsButtons: source.container.onUpdateTabsButtons,
openedTabs: source.container.openedTabs()
onUpdateTabsButtons: source.container.onUpdateTabsButtons
});
source.container.openedTabs.push(userDefinedFunctionTab);
// Activate
userDefinedFunctionTab.onTabClick();
source.container.tabsManager.activateNewTab(userDefinedFunctionTab);
}
public open = () => {
this.select();
let userDefinedFunctionTab: ViewModels.Tab = this.container
.openedTabs()
.filter(tab => tab.node && tab.node.rid === this.rid)[0];
if (!userDefinedFunctionTab) {
const userDefinedFunctionTabs: UserDefinedFunctionTab[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.UserDefinedFunctions,
(tab: ViewModels.Tab) => tab.collection && tab.collection.rid === this.rid
) as UserDefinedFunctionTab[];
let userDefinedFunctionTab: UserDefinedFunctionTab = userDefinedFunctionTabs && userDefinedFunctionTabs[0];
if (userDefinedFunctionTab) {
this.container.tabsManager.activateTab(userDefinedFunctionTab);
} else {
const userDefinedFunctionData = <DataModels.UserDefinedFunction>{
_rid: this.rid,
_self: this.self,
@ -86,14 +85,11 @@ export default class UserDefinedFunction implements ViewModels.UserDefinedFuncti
)}/udfs/${this.id()}`,
selfLink: "",
isActive: ko.observable(false),
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
openedTabs: this.container.openedTabs()
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.openedTabs.push(userDefinedFunctionTab);
}
// Activate
userDefinedFunctionTab.onTabClick();
this.container.tabsManager.activateNewTab(userDefinedFunctionTab);
}
};
public select() {
@ -119,7 +115,9 @@ export default class UserDefinedFunction implements ViewModels.UserDefinedFuncti
};
this.container.documentClientUtility.deleteUserDefinedFunction(this.collection, userDefinedFunctionData).then(
() => {
this.container.openedTabs.remove((tab: ViewModels.Tab) => tab.node && tab.node.rid === this.rid);
this.container.tabsManager.removeTabByComparator(
(tab: ViewModels.Tab) => tab.node && tab.node.rid === this.rid
);
this.collection.children.remove(this);
},
reason => {}

View File

@ -390,17 +390,15 @@ export class TabRouteHandler {
isNewScriptTab?: boolean
): ViewModels.Tab {
const explorer: ViewModels.Explorer = (<any>window).dataExplorer;
return _.find(explorer.openedTabs(), (tab: ViewModels.Tab) => {
const isMatchingTabKind =
tab.collection.databaseId === databaseId && tab.collection.id() === collectionId && tab.tabKind === tabKind;
if (isNewScriptTab) {
return isMatchingTabKind && (tab as ViewModels.ScriptTab).isNew();
}
return isMatchingTabKind;
});
const matchingTabs: ViewModels.Tab[] = explorer.tabsManager.getTabs(
tabKind,
(tab: ViewModels.Tab) =>
tab.collection &&
tab.collection.databaseId === databaseId &&
tab.collection.id() === collectionId &&
(!isNewScriptTab || (tab as ViewModels.ScriptTab).isNew())
);
return matchingTabs && matchingTabs[0];
}
private _findMatchingCollectionForResource(databaseId: string, collectionId: string): ViewModels.Collection {

View File

@ -187,171 +187,13 @@
<div
class="connectExplorerContainer"
data-bind="visible: !isRefreshingExplorer() && (openedTabs == null || openedTabs().length === 0)"
data-bind="visible: !isRefreshingExplorer() && tabsManager.openedTabs().length === 0"
>
<form class="connectExplorerFormContainer">
<div class="connectExplorer" data-bind="react: splashScreenAdapter"></div>
</form>
</div>
<!-- Tabs and Tabs Panes - Start -->
<div
id="content"
class="flexContainer hideOverflows"
data-bind="
visible: openedTabs && openedTabs().length > 0"
>
<!-- Tabs - Start -->
<div class="nav-tabs-margin" data-bind="visible:!isTabsContentExpanded()">
<ul class="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
<!-- ko foreach: openedTabs -->
<li
class="tabList"
data-bind="
attr: {
title: $data.tabPath,
'aria-selected': $data.isActive,
'aria-expanded': $data.isActive,
'aria-controls': $data.tabId
},
css:{
active: $data.isActive
},
hasFocus: $data.hasFocus,
event: { keypress: onKeyPressActivate },
click: $data.onTabClick,"
tabindex="0"
role="tab"
>
<span class="tabNavContentContainer">
<a data-toggle="tab" data-bind="attr: { href: '#' + $data.tabId }" tabindex="-1">
<div class="tab_Content">
<span class="statusIconContainer">
<div
class="errorIconContainer"
id="errorStatusIcon"
title="Click to view more details"
role="button"
data-bind="
click: onErrorDetailsClick,
event: { keypress: onErrorDetailsKeyPress },
attr: { tabindex: errorDetailsTabIndex },
css: { actionsEnabled: isActive },
visible: isExecutionError"
>
<span class="errorIcon"></span>
</div>
<img
class="loadingIcon"
title="Loading"
src="/circular_loader_black_16x16.gif"
data-bind="visible: $data.isExecuting"
alt="Loading"
/>
</span>
<span class="tabNavText" data-bind="text: $data.tabTitle"></span>
<span class="tabIconSection">
<span
aria-label="Close Tab"
role="button"
class="cancelButton"
data-bind="
click: $data.onCloseTabButtonClick,
event: { keypress: onKeyPressClose },
attr: { tabindex: $data.closeButtonTabIndex },
visible: $data.isActive() || $data.isMouseOver()"
title="Close"
>
<span
class="tabIcon close-Icon"
data-bind="visible: $data.isActive() || $data.isMouseOver()"
>
<img src="../images/close-black.svg" title="Close" alt="Close" />
</span>
</span>
</span>
</div>
</a>
</span>
</li>
<!-- /ko -->
</ul>
</div>
<!-- Tabs -- End -->
<!-- Tabs Panes -- Start -->
<div class="tabPanesContainer">
<!-- ko foreach: openedTabs -->
<div class="tabs-container" data-bind="visible: $data.isActive">
<!-- ko if: $data.tabKind === 0 -->
<documents-tab params="{data: $data}"></documents-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 1 -->
<settings-tab params="{data: $data}"></settings-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 2 -->
<stored-procedure-tab params="{data: $data}"></stored-procedure-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 3 -->
<user-defined-function-tab params="{data: $data}"></user-defined-function-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 4 -->
<trigger-tab params="{data: $data}"></trigger-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 5 -->
<query-tab params="{data: $data}"></query-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 6 -->
<graph-tab params="{data: $data}"></graph-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 9 -->
<tables-query-tab class="flexContainer" params="{data: $data}"></tables-query-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 10 -->
<mongo-shell-tab params="{data: $data}"></mongo-shell-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 11 -->
<database-settings-tab params="{data: $data}"></database-settings-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 12 -->
<conflicts-tab params="{data: $data}"></conflicts-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 14 -->
<terminal-tab params="{data: $data}"></terminal-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 15 -->
<notebookv2-tab params="{data: $data}"></notebookv2-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 16 -->
<spark-master-tab params="{data: $data}"></spark-master-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 17 -->
<gallery-tab params="{data: $data}"></gallery-tab>
<!-- /ko -->
<!-- ko if: $data.tabKind === 18 -->
<notebook-viewer-tab params="{data: $data}"></notebook-viewer-tab>
<!-- /ko -->
</div>
<!-- /ko -->
</div>
<!-- Tabs Panes - End -->
</div>
<!-- Tabs and Tabs Panes - End -->
<tabs-manager class="tabsContainer" params="{data: tabsManager}"></tabs-manager>
</div>
<!-- Collections Tree and Tabs - End -->
<div