Move selectedNode to zustand (#903)

This commit is contained in:
victor-meng 2021-06-24 11:56:33 -07:00 committed by GitHub
parent a7239c7579
commit 4d2a6999d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 481 additions and 955 deletions

View File

@ -89,7 +89,6 @@ export interface Database extends TreeNode {
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
selectDatabase(): void;
expandDatabase(): Promise<void>;
collapseDatabase(): void;

View File

@ -21,6 +21,7 @@ import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmat
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import { useSelectedNode } from "./useSelectedNode";
export interface CollectionContextMenuButtonParams {
databaseId: string;
@ -48,10 +49,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel explorer={container} selectedDatabase={container.findSelectedDatabase()} />
),
.openSidePanel("Delete " + getDatabaseName(), <DeleteDatabaseConfirmationPanel explorer={container} />),
label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem",
});
@ -82,7 +80,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: HostedTerminalIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
@ -97,7 +95,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddStoredProcedureIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
label: "New Stored Procedure",
@ -106,7 +104,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddUdfIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, undefined);
},
label: "New UDF",
@ -115,7 +113,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddTriggerIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
},
label: "New Trigger",

View File

@ -126,7 +126,6 @@ describe("SettingsComponent", () => {
isDatabaseExpanded: undefined,
isDatabaseShared: ko.computed(() => true),
selectedSubnodeKind: undefined,
selectDatabase: undefined,
expandDatabase: undefined,
collapseDatabase: undefined,
loadCollections: undefined,

View File

@ -36,7 +36,6 @@ describe("SettingsUtils", () => {
isDatabaseExpanded: ko.observable(false),
isDatabaseShared: ko.computed(() => true),
selectedSubnodeKind: ko.observable(undefined),
selectDatabase: undefined,
expandDatabase: undefined,
collapseDatabase: undefined,
loadCollections: undefined,

View File

@ -34,7 +34,6 @@ exports[`SettingsComponent renders 1`] = `
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
@ -59,8 +58,6 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
@ -123,7 +120,6 @@ exports[`SettingsComponent renders 1`] = `
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
@ -148,8 +144,6 @@ exports[`SettingsComponent renders 1`] = `
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],

View File

@ -67,6 +67,7 @@ import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure";
import { useDatabases } from "./useDatabases";
import { useSelectedNode } from "./useSelectedNode";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@ -83,14 +84,10 @@ export default class Explorer {
public tableDataClient: TableDataClient;
// Resource Tree
public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>;
public selectedNode: ko.Observable<ViewModels.TreeNode>;
private resourceTree: ResourceTreeAdapter;
// Resource Token
public resourceTokenCollection: ko.Observable<ViewModels.CollectionBase>;
public isResourceTokenCollectionNodeSelected: ko.Computed<boolean>;
public resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken;
// Tabs
@ -163,18 +160,10 @@ export default class Explorer {
this.resourceTokenCollection = ko.observable<ViewModels.CollectionBase>();
this.isSchemaEnabled = ko.computed<boolean>(() => userContext.features.enableSchema);
this.selectedNode = ko.observable<ViewModels.TreeNode>();
this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => {
useSelectedNode.subscribe(() => {
// Make sure switching tabs restores tabs display
this.isTabsContentExpanded(false);
});
this.isResourceTokenCollectionNodeSelected = ko.computed<boolean>(() => {
return (
this.selectedNode() &&
this.resourceTokenCollection() &&
this.selectedNode().id() === this.resourceTokenCollection().id()
);
});
this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => {
if (userContext.features.enableFixedCollectionWithSharedThroughput) {
@ -187,31 +176,11 @@ export default class Explorer {
return isCapabilityEnabled("EnableMongo");
});
this.selectedDatabaseId = ko.computed<string>(() => {
const selectedNode = this.selectedNode();
if (!selectedNode) {
return "";
}
switch (selectedNode.nodeKind) {
case "Collection":
return (selectedNode as ViewModels.CollectionBase).databaseId || "";
case "Database":
return selectedNode.id() || "";
case "DocumentId":
case "StoredProcedure":
case "Trigger":
case "UserDefinedFunction":
return selectedNode.collection.databaseId || "";
default:
return "";
}
});
this.tabsManager = params?.tabsManager ?? new TabsManager();
this.tabsManager.openedTabs.subscribe((tabs) => {
if (tabs.length === 0) {
this.selectedNode(undefined);
useSelectedNode.getState().setSelectedNode(undefined);
useCommandBar.getState().setContextButtons([]);
}
});
@ -363,22 +332,6 @@ export default class Explorer {
// TODO: return result
}
public isDatabaseNodeOrNoneSelected(): boolean {
return this.isNoneSelected() || this.isDatabaseNodeSelected();
}
public isDatabaseNodeSelected(): boolean {
return (this.selectedNode() && this.selectedNode().nodeKind === "Database") || false;
}
public isNodeKindSelected(nodeKind: string): boolean {
return (this.selectedNode() && this.selectedNode().nodeKind === nodeKind) || false;
}
public isNoneSelected(): boolean {
return this.selectedNode() == null;
}
public refreshDatabaseForResourceToken(): Promise<void> {
const databaseId = userContext.parsedResourceToken?.databaseId;
const collectionId = userContext.parsedResourceToken?.collectionId;
@ -388,7 +341,7 @@ export default class Explorer {
return readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection));
this.selectedNode(this.resourceTokenCollection());
useSelectedNode.getState().setSelectedNode(this.resourceTokenCollection());
});
}
@ -414,11 +367,9 @@ export default class Explorer {
},
startKey
);
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
const deltaDatabases = this.getDeltaDatabases(databases);
this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode);
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then(
() => {
deferred.resolve();
@ -611,34 +562,6 @@ export default class Explorer {
}
};
public findSelectedDatabase(): ViewModels.Database {
if (!this.selectedNode()) {
return null;
}
if (this.selectedNode().nodeKind === "Database") {
return _.find(
useDatabases.getState().databases,
(database: ViewModels.Database) => database.id() === this.selectedNode().id()
);
}
return this.findSelectedCollection().database;
}
public isSelectedDatabaseShared(): boolean {
const database = this.findSelectedDatabase();
if (!!database) {
return database.offer && !!database.offer();
}
return false;
}
public findSelectedCollection(): ViewModels.Collection {
return (this.selectedNode().nodeKind === "Collection"
? this.selectedNode()
: this.selectedNode().collection) as ViewModels.Collection;
}
private refreshAndExpandNewDatabases(newDatabases: ViewModels.Database[]): Q.Promise<void> {
// we reload collections for all databases so the resource tree reflects any collection-level changes
// i.e addition of stored procedures, etc.
@ -1331,7 +1254,7 @@ export default class Explorer {
}
public openUploadItemsPanePane(): void {
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane explorer={this} />);
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
}
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
useSidePanel

View File

@ -8,9 +8,9 @@ import * as React from "react";
import create, { UseStore } from "zustand";
import { StyleConstants } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useObservable } from "../../../hooks/useObservable";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
@ -29,13 +29,13 @@ export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
}));
export const CommandBar: React.FC<Props> = ({ container }: Props) => {
useObservable(container.selectedNode);
const selectedNodeState = useSelectedNode();
const buttons = useCommandBar((state) => state.contextButtons);
const backgroundColor = StyleConstants.BaseLight;
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container);
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
const contextButtons = (buttons || []).concat(
CommandBarComponentButtonFactory.createContextCommandBarButtons(container)
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState)
);
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container);

View File

@ -1,17 +1,22 @@
import * as ko from "knockout";
import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { CollectionBase } from "../../../Contracts/ViewModels";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import NotebookManager from "../../Notebook/NotebookManager";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: Explorer;
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
describe("Enable Azure Synapse Link Button", () => {
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@ -23,14 +28,12 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
});
it("Account is not serverless - button should be visible", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@ -45,7 +48,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
@ -55,6 +58,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Enable notebook button", () => {
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@ -67,9 +71,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
});
afterEach(() => {
@ -82,7 +83,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
@ -94,7 +95,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeUndefined();
});
@ -103,7 +104,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
expect(enableNotebookBtn.disabled).toBe(false);
@ -114,7 +115,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableNotebookBtn = buttons.find((button) => button.commandButtonLabel === enableNotebookBtnLabel);
expect(enableNotebookBtn).toBeDefined();
expect(enableNotebookBtn.disabled).toBe(true);
@ -126,6 +127,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Open Mongo Shell button", () => {
const openMongoShellBtnLabel = "Open Mongo Shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@ -137,9 +139,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isShellEnabled = ko.observable(true);
});
@ -163,7 +162,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
updateUserContext({
apiType: "SQL",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@ -173,13 +172,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@ -187,7 +186,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is not enabled and is available - button should be hidden", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@ -195,7 +194,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
@ -206,7 +205,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
expect(openMongoShellBtn.disabled).toBe(false);
@ -218,7 +217,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
mockExplorer.isShellEnabled = ko.observable(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
@ -226,6 +225,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Open Cassandra Shell button", () => {
const openCassandraShellBtnLabel = "Open Cassandra Shell";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@ -237,8 +237,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
});
beforeEach(() => {
@ -262,7 +260,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
} as DatabaseAccount,
});
console.log(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
@ -272,13 +270,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
portalEnv: "mooncake",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
@ -286,7 +284,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
});
@ -294,7 +292,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
@ -305,7 +303,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
expect(openCassandraShellBtn.disabled).toBe(false);
@ -316,6 +314,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("GitHub buttons", () => {
const connectToGitHubBtnLabel = "Connect to GitHub";
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
@ -328,7 +327,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.notebookManager = new NotebookManager();
@ -346,7 +344,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeDefined();
});
@ -355,7 +353,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.notebookManager.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const manageGitHubSettingsBtn = buttons.find(
(button) => button.commandButtonLabel === manageGitHubSettingsBtnLabel
);
@ -363,7 +361,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const connectToGitHubBtn = buttons.find((button) => button.commandButtonLabel === connectToGitHubBtnLabel);
expect(connectToGitHubBtn).toBeUndefined();
@ -376,10 +374,12 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
describe("Resource token", () => {
const mockCollection = { id: ko.observable("test") } as CollectionBase;
useSelectedNode.getState().setSelectedNode(mockCollection);
const selectedNodeState = useSelectedNode.getState();
beforeAll(() => {
mockExplorer = {} as Explorer;
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.resourceTokenCollection = ko.observable(mockCollection);
updateUserContext({
authType: AuthType.ResourceToken,
@ -392,7 +392,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
kind: "DocumentDB",
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
expect(buttons.length).toBe(2);
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
expect(buttons[0].disabled).toBe(false);

View File

@ -31,12 +31,16 @@ import Explorer from "../../Explorer";
import { OpenFullScreen } from "../../OpenFullScreen";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
import { SelectedNodeState } from "../../useSelectedNode";
let counter = 0;
export function createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
export function createStaticCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
if (userContext.authType === AuthType.ResourceToken) {
return createStaticCommandBarButtonsForResourceToken(container);
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
}
const newCollectionBtn = createNewCollectionGroup(container);
@ -71,7 +75,9 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createNotebookWorkspaceResetButton(container));
if (
(userContext.apiType === "Mongo" && container.isShellEnabled() && container.isDatabaseNodeOrNoneSelected()) ||
(userContext.apiType === "Mongo" &&
container.isShellEnabled() &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
buttons.push(createDivider());
@ -87,18 +93,18 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
}
}
if (!container.isDatabaseNodeOrNoneSelected()) {
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isQuerySupported) {
buttons.push(createDivider());
const newSqlQueryBtn = createNewSQLQueryButton(container);
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
buttons.push(newSqlQueryBtn);
}
if (isQuerySupported && container.selectedNode() && container.findSelectedCollection()) {
if (isQuerySupported && selectedNodeState.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
buttons.push(openQueryBtn);
}
@ -108,16 +114,16 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
newStoredProcedureBtn.children = createScriptCommandButtons(container);
newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState);
buttons.push(newStoredProcedureBtn);
}
}
@ -125,16 +131,19 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
return buttons;
}
export function createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
export function createContextCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
@ -144,7 +153,7 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
};
buttons.push(newMongoShellBtn);
}
@ -279,20 +288,20 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
};
}
function createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandButtonComponentProps {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
const label = "New SQL Query";
return {
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
} else if (userContext.apiType === "Mongo") {
const label = "New Query";
@ -300,23 +309,24 @@ function createNewSQLQueryButton(container: Explorer): CommandButtonComponentPro
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
}
return undefined;
}
export function createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
export function createScriptCommandButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean = !container.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
const shouldEnableScriptsCommands: boolean =
!selectedNodeState.isDatabaseNodeOrNoneSelected() && areScriptsSupported();
if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure";
@ -324,13 +334,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newStoredProcedureBtn);
}
@ -341,13 +351,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
iconSrc: AddUdfIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newUserDefinedFunctionBtn);
}
@ -358,13 +368,13 @@ export function createScriptCommandButtons(container: Explorer): CommandButtonCo
iconSrc: AddTriggerIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
};
buttons.push(newTriggerBtn);
}
@ -411,12 +421,12 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
};
}
function createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps {
function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
const label = "Open Query From Disk";
return {
iconSrc: OpenQueryFromDiskIcon,
iconAlt: label,
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane explorer={container} />),
onCommandClick: () => useSidePanel.getState().openSidePanel("Load Query", <LoadQueryPane />),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
@ -537,19 +547,25 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
};
}
function createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] {
const newSqlQueryBtn = createNewSQLQueryButton(container);
function createStaticCommandBarButtonsForResourceToken(
container: Explorer,
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState);
const openQueryBtn = createOpenQueryButton(container);
newSqlQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
const isResourceTokenCollectionNodeSelected: boolean =
container.resourceTokenCollection() &&
container.resourceTokenCollection().id() === selectedNodeState.selectedNode?.id();
newSqlQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
newSqlQueryBtn.onCommandClick = () => {
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection();
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
};
openQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected;
if (!openQueryBtn.disabled) {
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
}
return [newSqlQueryBtn, openQueryBtn];

View File

@ -14,6 +14,7 @@ import {
import Explorer from "../../Explorer";
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
interface BrowseQueriesPaneProps {
explorer: Explorer;
@ -24,7 +25,7 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
}: BrowseQueriesPaneProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const loadSavedQuery = (savedQuery: Query): void => {
const selectedCollection: Collection = explorer && explorer.findSelectedCollection();
const selectedCollection: Collection = useSelectedNode.getState().findSelectedCollection();
if (!selectedCollection) {
// should never get into this state because this pane is only accessible through the query tab
logError("No collection was selected", "BrowseQueriesPane.loadSavedQuery");

View File

@ -1,17 +1,18 @@
jest.mock("../../../Common/dataAccess/deleteCollection");
jest.mock("../../../Shared/Telemetry/TelemetryProcessor");
import { mount, ReactWrapper, shallow } from "enzyme";
import { mount, shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../../Common/DeleteFeedback";
import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels";
import { Collection, Database, TreeNode } from "../../../Contracts/ViewModels";
import { Collection, Database } from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane";
describe("Delete Collection Confirmation Pane", () => {
@ -54,47 +55,39 @@ describe("Delete Collection Confirmation Pane", () => {
it("should return true if last collection and database does not have shared throughput else false", () => {
const fakeExplorer = new Explorer();
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isSelectedDatabaseShared = () => false;
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
collectionName: "container",
};
const wrapper = shallow(<DeleteCollectionConfirmationPane {...props} />);
const wrapper = shallow(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
const database = { id: ko.observable("testDB") } as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
database.nodeKind = "Database";
database.isDatabaseShared = ko.computed(() => false);
useDatabases.getState().addDatabases([database]);
wrapper.setProps(props);
useSelectedNode.getState().setSelectedNode(database);
wrapper.setProps({ explorer: fakeExplorer });
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
props.explorer.isSelectedDatabaseShared = () => true;
wrapper.setProps(props);
database.isDatabaseShared = ko.computed(() => true);
wrapper.setProps({ explorer: fakeExplorer });
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
});
});
describe("submit()", () => {
let wrapper: ReactWrapper;
const selectedCollectionId = "testCol";
const databaseId = "testDatabase";
const fakeExplorer = {} as Explorer;
fakeExplorer.findSelectedCollection = () => {
return {
id: ko.observable<string>(selectedCollectionId),
databaseId,
rid: "test",
} as Collection;
};
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
fakeExplorer.selectedNode = ko.observable<TreeNode>();
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isSelectedDatabaseShared = () => false;
const database = { id: ko.observable("testDB") } as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
useDatabases.getState().addDatabases([database]);
const database = { id: ko.observable(databaseId) } as Database;
const collection = {
id: ko.observable(selectedCollectionId),
nodeKind: "Collection",
database,
databaseId,
} as Collection;
database.collections = ko.observableArray<Collection>([collection]);
database.isDatabaseShared = ko.computed(() => false);
beforeAll(() => {
updateUserContext({
@ -112,15 +105,17 @@ describe("Delete Collection Confirmation Pane", () => {
});
beforeEach(() => {
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
collectionName: "container",
};
wrapper = mount(<DeleteCollectionConfirmationPane {...props} />);
useDatabases.getState().addDatabases([database]);
useSelectedNode.getState().setSelectedNode(collection);
});
afterEach(() => {
useDatabases.getState().clearDatabases();
useSelectedNode.getState().setSelectedNode(undefined);
});
it("should call delete collection", () => {
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
@ -137,6 +132,7 @@ describe("Delete Collection Confirmation Pane", () => {
});
it("should record feedback", async () => {
const wrapper = mount(<DeleteCollectionConfirmationPane explorer={fakeExplorer} />);
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
wrapper
.find("#confirmCollectionId")

View File

@ -14,6 +14,7 @@ import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface DeleteCollectionConfirmationPaneProps {
@ -24,19 +25,19 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
explorer,
}: DeleteCollectionConfirmationPaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const isLastCollection = useDatabases((state) => state.isLastCollection);
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
const [inputCollectionName, setInputCollectionName] = useState<string>("");
const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState(false);
const shouldRecordFeedback = (): boolean => {
return isLastCollection() && !explorer.isSelectedDatabaseShared();
};
const shouldRecordFeedback = (): boolean =>
useDatabases.getState().isLastCollection() &&
!useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared();
const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = "Delete " + collectionName;
const onSubmit = async (): Promise<void> => {
const collection = explorer.findSelectedCollection();
const collection = useSelectedNode.getState().findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
setFormError(errorMessage);
@ -61,7 +62,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
await deleteCollection(collection.databaseId, collection.id());
setIsExecuting(false);
explorer.selectedNode(collection.database);
useSelectedNode.getState().setSelectedNode(collection.database);
explorer.tabsManager?.closeTabsByComparator(
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
);

View File

@ -2,15 +2,9 @@
exports[`Delete Collection Confirmation Pane submit() should call delete collection 1`] = `
<DeleteCollectionConfirmationPane
closePanel={[Function]}
collectionName="container"
explorer={
Object {
"findSelectedCollection": [Function],
"isSelectedDatabaseShared": [Function],
"refreshAllDatabases": [Function],
"selectedCollectionId": [Function],
"selectedNode": [Function],
}
}
>
@ -372,355 +366,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</TextFieldBase>
</StyledTextFieldBase>
</div>
<div
className="deleteCollectionFeedback"
>
<Text
block={true}
variant="small"
>
<span
className="css-66"
>
Help us improve Azure Cosmos DB!
</span>
</Text>
<Text
block={true}
variant="small"
>
<span
className="css-66"
>
What is the reason why you are deleting this
container
?
</span>
</Text>
<StyledTextFieldBase
id="deleteCollectionFeedbackInput"
multiline={true}
onChange={[Function]}
rows={3}
styles={
Object {
"fieldGroup": Object {
"width": 300,
},
}
}
value=""
>
<TextFieldBase
deferredValidationTime={200}
id="deleteCollectionFeedbackInput"
multiline={true}
onChange={[Function]}
resizable={true}
rows={3}
styles={[Function]}
theme={
Object {
"disableGlobalClassNames": false,
"effects": Object {
"elevation16": "0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108)",
"elevation4": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
"elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)",
"elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"roundedCorner2": "2px",
"roundedCorner4": "4px",
"roundedCorner6": "6px",
},
"fonts": Object {
"large": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "18px",
"fontWeight": 400,
},
"medium": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "14px",
"fontWeight": 400,
},
"mediumPlus": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "16px",
"fontWeight": 400,
},
"mega": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "68px",
"fontWeight": 600,
},
"small": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "12px",
"fontWeight": 400,
},
"smallPlus": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "12px",
"fontWeight": 400,
},
"superLarge": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "42px",
"fontWeight": 600,
},
"tiny": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "10px",
"fontWeight": 400,
},
"xLarge": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "20px",
"fontWeight": 600,
},
"xLargePlus": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "24px",
"fontWeight": 600,
},
"xSmall": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "10px",
"fontWeight": 400,
},
"xxLarge": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "28px",
"fontWeight": 600,
},
"xxLargePlus": Object {
"MozOsxFontSmoothing": "grayscale",
"WebkitFontSmoothing": "antialiased",
"fontFamily": "'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif",
"fontSize": "32px",
"fontWeight": 600,
},
},
"isInverted": false,
"palette": Object {
"accent": "#0078d4",
"black": "#000000",
"blackTranslucent40": "rgba(0,0,0,.4)",
"blue": "#0078d4",
"blueDark": "#002050",
"blueLight": "#00bcf2",
"blueMid": "#00188f",
"green": "#107c10",
"greenDark": "#004b1c",
"greenLight": "#bad80a",
"magenta": "#b4009e",
"magentaDark": "#5c005c",
"magentaLight": "#e3008c",
"neutralDark": "#201f1e",
"neutralLight": "#edebe9",
"neutralLighter": "#f3f2f1",
"neutralLighterAlt": "#faf9f8",
"neutralPrimary": "#323130",
"neutralPrimaryAlt": "#3b3a39",
"neutralQuaternary": "#d2d0ce",
"neutralQuaternaryAlt": "#e1dfdd",
"neutralSecondary": "#605e5c",
"neutralSecondaryAlt": "#8a8886",
"neutralTertiary": "#a19f9d",
"neutralTertiaryAlt": "#c8c6c4",
"orange": "#d83b01",
"orangeLight": "#ea4300",
"orangeLighter": "#ff8c00",
"purple": "#5c2d91",
"purpleDark": "#32145a",
"purpleLight": "#b4a0ff",
"red": "#e81123",
"redDark": "#a4262c",
"teal": "#008272",
"tealDark": "#004b50",
"tealLight": "#00b294",
"themeDark": "#005a9e",
"themeDarkAlt": "#106ebe",
"themeDarker": "#004578",
"themeLight": "#c7e0f4",
"themeLighter": "#deecf9",
"themeLighterAlt": "#eff6fc",
"themePrimary": "#0078d4",
"themeSecondary": "#2b88d8",
"themeTertiary": "#71afe5",
"white": "#ffffff",
"whiteTranslucent40": "rgba(255,255,255,.4)",
"yellow": "#ffb900",
"yellowDark": "#d29200",
"yellowLight": "#fff100",
},
"rtl": undefined,
"semanticColors": Object {
"accentButtonBackground": "#0078d4",
"accentButtonText": "#ffffff",
"actionLink": "#323130",
"actionLinkHovered": "#201f1e",
"blockingBackground": "#FDE7E9",
"blockingIcon": "#FDE7E9",
"bodyBackground": "#ffffff",
"bodyBackgroundChecked": "#edebe9",
"bodyBackgroundHovered": "#f3f2f1",
"bodyDivider": "#edebe9",
"bodyFrameBackground": "#ffffff",
"bodyFrameDivider": "#edebe9",
"bodyStandoutBackground": "#faf9f8",
"bodySubtext": "#605e5c",
"bodyText": "#323130",
"bodyTextChecked": "#000000",
"buttonBackground": "#ffffff",
"buttonBackgroundChecked": "#c8c6c4",
"buttonBackgroundCheckedHovered": "#edebe9",
"buttonBackgroundDisabled": "#f3f2f1",
"buttonBackgroundHovered": "#f3f2f1",
"buttonBackgroundPressed": "#edebe9",
"buttonBorder": "#8a8886",
"buttonBorderDisabled": "#f3f2f1",
"buttonText": "#323130",
"buttonTextChecked": "#201f1e",
"buttonTextCheckedHovered": "#000000",
"buttonTextDisabled": "#a19f9d",
"buttonTextHovered": "#201f1e",
"buttonTextPressed": "#201f1e",
"cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
"cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"cardStandoutBackground": "#ffffff",
"defaultStateBackground": "#faf9f8",
"disabledBackground": "#f3f2f1",
"disabledBodySubtext": "#c8c6c4",
"disabledBodyText": "#a19f9d",
"disabledBorder": "#c8c6c4",
"disabledSubtext": "#d2d0ce",
"disabledText": "#a19f9d",
"errorBackground": "#FDE7E9",
"errorIcon": "#A80000",
"errorText": "#a4262c",
"focusBorder": "#605e5c",
"infoBackground": "#f3f2f1",
"infoIcon": "#605e5c",
"inputBackground": "#ffffff",
"inputBackgroundChecked": "#0078d4",
"inputBackgroundCheckedHovered": "#005a9e",
"inputBorder": "#605e5c",
"inputBorderHovered": "#323130",
"inputFocusBorderAlt": "#0078d4",
"inputForegroundChecked": "#ffffff",
"inputIcon": "#0078d4",
"inputIconDisabled": "#a19f9d",
"inputIconHovered": "#005a9e",
"inputPlaceholderBackgroundChecked": "#deecf9",
"inputPlaceholderText": "#605e5c",
"inputText": "#323130",
"inputTextHovered": "#201f1e",
"link": "#0078d4",
"linkHovered": "#004578",
"listBackground": "#ffffff",
"listHeaderBackgroundHovered": "#f3f2f1",
"listHeaderBackgroundPressed": "#edebe9",
"listItemBackgroundChecked": "#edebe9",
"listItemBackgroundCheckedHovered": "#e1dfdd",
"listItemBackgroundHovered": "#f3f2f1",
"listText": "#323130",
"listTextColor": "#323130",
"menuBackground": "#ffffff",
"menuDivider": "#c8c6c4",
"menuHeader": "#0078d4",
"menuIcon": "#0078d4",
"menuItemBackgroundChecked": "#edebe9",
"menuItemBackgroundHovered": "#f3f2f1",
"menuItemBackgroundPressed": "#edebe9",
"menuItemText": "#323130",
"menuItemTextHovered": "#201f1e",
"messageLink": "#005A9E",
"messageLinkHovered": "#004578",
"messageText": "#323130",
"primaryButtonBackground": "#0078d4",
"primaryButtonBackgroundDisabled": "#f3f2f1",
"primaryButtonBackgroundHovered": "#106ebe",
"primaryButtonBackgroundPressed": "#005a9e",
"primaryButtonBorder": "transparent",
"primaryButtonText": "#ffffff",
"primaryButtonTextDisabled": "#d2d0ce",
"primaryButtonTextHovered": "#ffffff",
"primaryButtonTextPressed": "#ffffff",
"severeWarningBackground": "#FED9CC",
"severeWarningIcon": "#D83B01",
"smallInputBorder": "#605e5c",
"successBackground": "#DFF6DD",
"successIcon": "#107C10",
"successText": "#107C10",
"variantBorder": "#edebe9",
"variantBorderHovered": "#a19f9d",
"warningBackground": "#FFF4CE",
"warningHighlight": "#ffb900",
"warningIcon": "#797775",
"warningText": "#323130",
},
"spacing": Object {
"l1": "20px",
"l2": "32px",
"m": "16px",
"s1": "8px",
"s2": "4px",
},
}
}
validateOnLoad={true}
value=""
>
<div
className="ms-TextField ms-TextField--multiline root-55"
>
<div
className="ms-TextField-wrapper"
>
<div
className="ms-TextField-fieldGroup fieldGroup-67"
>
<textarea
aria-invalid={false}
className="ms-TextField-field field-68"
id="deleteCollectionFeedbackInput"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onInput={[Function]}
rows={3}
value=""
/>
</div>
</div>
</div>
</TextFieldBase>
</StyledTextFieldBase>
</div>
</div>
</div>
<PanelFooterComponent
@ -2433,7 +2078,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<button
aria-label="OK"
className="ms-Button ms-Button--primary root-70"
className="ms-Button ms-Button--primary root-66"
data-is-focusable={true}
id="sidePanelOkButton"
onClick={[Function]}
@ -2445,16 +2090,16 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
type="submit"
>
<span
className="ms-Button-flexContainer flexContainer-71"
className="ms-Button-flexContainer flexContainer-67"
data-automationid="splitbuttonprimary"
>
<span
className="ms-Button-textContainer textContainer-72"
className="ms-Button-textContainer textContainer-68"
>
<span
className="ms-Button-label label-74"
id="id__6"
key="id__6"
className="ms-Button-label label-70"
id="id__3"
key="id__3"
>
OK
</span>

View File

@ -1,6 +1,6 @@
jest.mock("../../Common/dataAccess/deleteDatabase");
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
import { mount, ReactWrapper, shallow } from "enzyme";
import { mount, shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
@ -13,49 +13,14 @@ import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import { TabsManager } from "../Tabs/TabsManager";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel";
describe("Delete Database Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => {
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
const fakeExplorer = {} as Explorer;
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isSelectedDatabaseShared = () => false;
const selectedDatabaseId = "testDatabase";
let fakeExplorer: Explorer;
let database: Database;
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
database.id = ko.observable<string>("testDatabase");
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
selectedDatabase: database,
};
const wrapper = shallow(<DeleteDatabaseConfirmationPanel {...props} />);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
useDatabases.getState().addDatabases([database]);
wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
useDatabases.getState().clearDatabases();
});
});
describe("submit()", () => {
const selectedDatabaseId = "testDatabse";
const database = { id: ko.observable("testDatabase") } as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
database.id = ko.observable<string>(selectedDatabaseId);
const fakeExplorer = {} as Explorer;
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isSelectedDatabaseShared = () => false;
fakeExplorer.tabsManager = new TabsManager();
fakeExplorer.selectedNode = ko.observable();
let wrapper: ReactWrapper;
beforeAll(() => {
updateUserContext({
databaseAccount: {
@ -69,23 +34,39 @@ describe("Delete Database Confirmation Pane", () => {
});
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);
useDatabases.getState().addDatabases([database]);
});
beforeEach(() => {
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
selectedDatabase: database,
};
fakeExplorer = {} as Explorer;
fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.tabsManager = new TabsManager();
wrapper = mount(<DeleteDatabaseConfirmationPanel {...props} />);
database = {} as Database;
database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
database.id = ko.observable<string>(selectedDatabaseId);
database.nodeKind = "Database";
useDatabases.getState().addDatabases([database]);
useSelectedNode.getState().setSelectedNode(database);
});
afterAll(() => useDatabases.getState().clearDatabases());
afterEach(() => {
useDatabases.getState().clearDatabases();
useSelectedNode.getState().setSelectedNode(undefined);
});
it("shouldRecordFeedback() should return true if last non empty database or is last database that has shared throughput", () => {
const wrapper = shallow(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
useDatabases.getState().addDatabases([database]);
wrapper.setProps({ explorer: fakeExplorer });
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
useDatabases.getState().clearDatabases();
});
it("Should call delete database", () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
@ -100,6 +81,7 @@ describe("Delete Database Confirmation Pane", () => {
});
it("should record feedback", async () => {
const wrapper = mount(<DeleteDatabaseConfirmationPanel explorer={fakeExplorer} />);
expect(wrapper.exists("#confirmDatabaseId")).toBe(true);
wrapper
.find("#confirmDatabaseId")
@ -129,5 +111,4 @@ describe("Delete Database Confirmation Pane", () => {
});
wrapper.unmount();
});
});
});

View File

@ -14,17 +14,16 @@ import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
interface DeleteDatabaseConfirmationPanelProps {
explorer: Explorer;
selectedDatabase: Database;
}
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
explorer,
selectedDatabase,
}: DeleteDatabaseConfirmationPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase);
@ -33,6 +32,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const [formError, setFormError] = useState<string>("");
const [databaseInput, setDatabaseInput] = useState<string>("");
const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>("");
const selectedDatabase: Database = useSelectedNode.getState().findSelectedDatabase();
const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
@ -54,7 +54,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
closeSidePanel();
explorer.refreshAllDatabases();
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
explorer.selectedNode(undefined);
useSelectedNode.getState().setSelectedNode(undefined);
selectedDatabase
.collections()
.forEach((collection: Collection) =>

View File

@ -23,7 +23,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
@ -48,8 +47,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],

View File

@ -1,17 +1,10 @@
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { LoadQueryPane } from "./LoadQueryPane";
describe("Load Query Pane", () => {
it("should render Default properly", () => {
const fakeExplorer = {} as Explorer;
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
};
const wrapper = shallow(<LoadQueryPane {...props} />);
const wrapper = shallow(<LoadQueryPane />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -7,14 +7,10 @@ import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
interface LoadQueryPaneProps {
explorer: Explorer;
}
export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({ explorer }: LoadQueryPaneProps): JSX.Element => {
export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>("");
@ -58,7 +54,7 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({ explorer
};
const loadQueryFromFile = async (file: File): Promise<void> => {
const selectedCollection: Collection = explorer?.findSelectedCollection();
const selectedCollection: Collection = useSelectedNode.getState().findSelectedCollection();
const reader = new FileReader();
let fileData: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -13,7 +13,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
@ -38,8 +37,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],

View File

@ -1,13 +1,10 @@
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { UploadItemsPane } from "./UploadItemsPane";
const props = {
explorer: new Explorer(),
};
describe("Upload Items Pane", () => {
it("should render Default properly", () => {
const wrapper = shallow(<UploadItemsPane {...props} />);
const wrapper = shallow(<UploadItemsPane />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -3,15 +3,11 @@ import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload/Upload";
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { getErrorMessage } from "../../Tables/Utilities";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface UploadItemsPaneProps {
explorer: Explorer;
}
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explorer }: UploadItemsPaneProps) => {
export const UploadItemsPane: FunctionComponent = () => {
const [files, setFiles] = useState<FileList>();
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
const [formError, setFormError] = useState<string>("");
@ -25,7 +21,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
return;
}
const selectedCollection = explorer.findSelectedCollection();
const selectedCollection = useSelectedNode.getState().findSelectedCollection();
setIsExecuting(true);
selectedCollection

View File

@ -1,26 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Delete Database Confirmation Pane submit() Should call delete database 1`] = `
exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<DeleteDatabaseConfirmationPanel
closePanel={[Function]}
explorer={
Object {
"isSelectedDatabaseShared": [Function],
"refreshAllDatabases": [Function],
"selectedNode": [Function],
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
},
}
}
openNotificationConsole={[Function]}
selectedDatabase={
Object {
"collections": [Function],
"id": [Function],
}
}
>
<RightPaneForm
formError=""

View File

@ -7,7 +7,6 @@ jest.mock("../Explorer");
const createExplorer = () => {
const mock = new Explorer();
mock.selectedNode = ko.observable();
mock.isNotebookEnabled = ko.observable(false);
mock.tabsManager = new TabsManager();
return mock as jest.Mocked<Explorer>;

View File

@ -23,6 +23,7 @@ import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
export interface SplashScreenItem {
iconSrc: string;
@ -60,7 +61,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public componentDidMount() {
this.subscriptions.push(
this.container.selectedNode.subscribe(() => this.setState({})),
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
this.container.isNotebookEnabled.subscribe(() => this.setState({}))
);
}
@ -228,12 +229,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
return items;
}
if (!this.container.isDatabaseNodeOrNoneSelected()) {
if (!useSelectedNode.getState().isDatabaseNodeOrNoneSelected()) {
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
items.push({
iconSrc: NewQueryIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
},
title: "New SQL Query",
@ -243,7 +244,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
items.push({
iconSrc: NewQueryIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null);
},
title: "New Query",
@ -266,20 +267,14 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "New Stored Procedure",
description: null,
onClick: () => {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
},
});
}
/* Scale & Settings */
let isShared = false;
if (this.container.isDatabaseNodeSelected()) {
isShared = this.container.findSelectedDatabase().isDatabaseShared();
} else if (this.container.isNodeKindSelected("Collection")) {
const database: ViewModels.Database = this.container.findSelectedCollection().getDatabase();
isShared = database && database.isDatabaseShared();
}
const isShared = useSelectedNode.getState().findSelectedDatabase()?.isDatabaseShared();
const label = isShared ? "Settings" : "Scale & Settings";
items.push({
@ -287,7 +282,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: label,
description: null,
onClick: () => {
const selectedCollection: ViewModels.Collection = this.container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && selectedCollection.onSettingsClick();
},
});

View File

@ -28,6 +28,7 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import DocumentId from "../Tree/DocumentId";
import { useSelectedNode } from "../useSelectedNode";
import template from "./DocumentsTab.html";
import TabsBase from "./TabsBase";
@ -911,13 +912,13 @@ export default class DocumentsTab extends TabsBase {
iconSrc: UploadIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
selectedCollection && container.openUploadItemsPanePane();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected(),
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
};
}
}

View File

@ -15,6 +15,7 @@ import { EditorReact } from "../../Controls/Editor/EditorReact";
import Explorer from "../../Explorer";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import StoredProcedure from "../../Tree/StoredProcedure";
import { useSelectedNode } from "../../useSelectedNode";
import ScriptTabBase from "../ScriptTabBase";
export interface IStorProcTabComponentAccessor {
@ -298,12 +299,13 @@ export default class StoredProcedureTabComponent extends React.Component<
}
const database: ViewModels.Database = this.props.collectionBase.getDatabase();
const setSelectedNode = useSelectedNode.getState().setSelectedNode;
if (!database.isDatabaseExpanded()) {
this.props.collectionBase.container.selectedNode(database);
setSelectedNode(database);
} else if (!this.props.collectionBase.isCollectionExpanded() || !this.collection.isStoredProceduresExpanded()) {
this.props.collectionBase.container.selectedNode(this.props.collectionBase);
setSelectedNode(this.props.collectionBase);
} else {
this.props.collectionBase.container.selectedNode(this.node);
setSelectedNode(this.node);
}
}

View File

@ -9,6 +9,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "../useSelectedNode";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import { TabsManager } from "./TabsManager";
// TODO: Use specific actions for logging telemetry data
@ -74,12 +75,13 @@ export default class TabsBase extends WaitsForTemplateViewModel {
protected updateSelectedNode(): void {
const relatedDatabase = (this.collection && this.collection.getDatabase()) || this.database;
const setSelectedNode = useSelectedNode.getState().setSelectedNode;
if (relatedDatabase && !relatedDatabase.isDatabaseExpanded()) {
this.getContainer().selectedNode(relatedDatabase);
setSelectedNode(relatedDatabase);
} else if (this.collection && !this.collection.isCollectionExpanded()) {
this.getContainer().selectedNode(this.collection);
setSelectedNode(this.collection);
} else {
this.getContainer().selectedNode(this.node);
setSelectedNode(this.node);
}
}

View File

@ -31,10 +31,6 @@ describe("Collection", () => {
function generateMockCollectionWithDataModel(data: DataModels.Collection): Collection {
const mockContainer = {} as Explorer;
mockContainer.isDatabaseNodeOrNoneSelected = () => {
return false;
};
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);
}

View File

@ -34,6 +34,7 @@ import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure";
@ -223,7 +224,7 @@ export default class Collection implements ViewModels.Collection {
}
public expandCollapseCollection() {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Collection node",
@ -276,7 +277,7 @@ export default class Collection implements ViewModels.Collection {
}
public onDocumentDBDocumentsClick() {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Documents node",
@ -321,7 +322,7 @@ export default class Collection implements ViewModels.Collection {
}
public onConflictsClick() {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Conflicts node",
@ -366,7 +367,7 @@ export default class Collection implements ViewModels.Collection {
}
public onTableEntitiesClick() {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.QueryTables);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Entities node",
@ -419,7 +420,7 @@ export default class Collection implements ViewModels.Collection {
}
public onGraphDocumentsClick() {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Documents node",
@ -470,7 +471,7 @@ export default class Collection implements ViewModels.Collection {
}
public onMongoDBDocumentsClick = () => {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Documents node",
@ -514,7 +515,7 @@ export default class Collection implements ViewModels.Collection {
};
public onSchemaAnalyzerClick = async () => {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
@ -557,7 +558,7 @@ export default class Collection implements ViewModels.Collection {
};
public onSettingsClick = async (): Promise<void> => {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Settings node",
@ -744,21 +745,21 @@ export default class Collection implements ViewModels.Collection {
public createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure {
const node = new StoredProcedure(this.container, this, data);
this.container.selectedNode(node);
useSelectedNode.getState().setSelectedNode(node);
this.children.push(node);
return node;
}
public createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction {
const node = new UserDefinedFunction(this.container, this, data);
this.container.selectedNode(node);
useSelectedNode.getState().setSelectedNode(node);
this.children.push(node);
return node;
}
public createTriggerNode(data: TriggerDefinition & Resource): Trigger {
const node = new Trigger(this.container, this, data);
this.container.selectedNode(node);
useSelectedNode.getState().setSelectedNode(node);
this.children.push(node);
return node;
}

View File

@ -18,6 +18,7 @@ import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import Collection from "./Collection";
export default class Database implements ViewModels.Database {
@ -53,7 +54,7 @@ export default class Database implements ViewModels.Database {
}
public onSettingsClick = () => {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Settings node",
@ -121,25 +122,6 @@ export default class Database implements ViewModels.Database {
}
};
public isDatabaseNodeSelected(): boolean {
return (
!this.isDatabaseExpanded() &&
this.container.selectedNode &&
this.container.selectedNode() &&
this.container.selectedNode().nodeKind === "Database" &&
this.container.selectedNode().id() === this.id()
);
}
public selectDatabase() {
this.container.selectedNode(this);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Database node",
dataExplorerArea: Constants.Areas.ResourceTree,
});
}
public async expandDatabase() {
if (this.isDatabaseExpanded()) {
return;

View File

@ -10,6 +10,7 @@ import DocumentsTab from "../Tabs/DocumentsTab";
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import DocumentId from "./DocumentId";
export default class ResourceTokenCollection implements ViewModels.CollectionBase {
@ -104,7 +105,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
}
public onDocumentDBDocumentsClick() {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Documents node",

View File

@ -1,114 +1,101 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import TabsBase from "../Tabs/TabsBase";
import { ResourceTreeAdapter } from "./ResourceTreeAdapter";
import { useSelectedNode } from "../useSelectedNode";
describe("ResourceTreeAdapter", () => {
const mockContainer = (): Explorer =>
(({
selectedNode: ko.observable<ViewModels.TreeNode>({
nodeKind: "nodeKind",
rid: "rid",
id: ko.observable<string>("id"),
}),
tabsManager: {
activeTab: ko.observable<TabsBase>({
describe("useSelectedNode.getState()", () => {
const mockTab = {
tabKind: ViewModels.CollectionTabKind.Documents,
} as TabsBase),
},
isNotebookEnabled: ko.observable<boolean>(true),
databases: ko.observable<ViewModels.Database[]>([]),
} as unknown) as Explorer);
} as TabsBase;
// TODO isDataNodeSelected needs a better design and refactor, but for now, we protect some of the code paths
describe("isDataNodeSelected", () => {
afterEach(() => useSelectedNode.getState().setSelectedNode(undefined));
it("it should not select if no selected node", () => {
const explorer = mockContainer();
explorer.selectedNode(undefined);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined);
const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(mockTab, "foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy();
});
it("it should not select incorrect subnodekinds", () => {
const resourceTreeAdapter = new ResourceTreeAdapter(mockContainer());
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined);
useSelectedNode.getState().setSelectedNode({
nodeKind: "nodeKind",
rid: "rid",
id: ko.observable<string>("id"),
});
const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(mockTab, "foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy();
});
it("it should not select if no active tab", () => {
const explorer = mockContainer();
explorer.tabsManager.activeTab(undefined);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined);
useSelectedNode.getState().setSelectedNode({
nodeKind: "nodeKind",
rid: "rid",
id: ko.observable<string>("id"),
});
const isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(undefined, "foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy();
});
it("should select if correct database node regardless of subnodekinds", () => {
const subNodeKind = ViewModels.CollectionTabKind.Documents;
const explorer = mockContainer();
explorer.selectedNode(({
useSelectedNode.getState().setSelectedNode({
nodeKind: "Database",
rid: "dbrid",
id: ko.observable<string>("dbid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as unknown) as ViewModels.TreeNode);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", undefined, [
ViewModels.CollectionTabKind.Documents,
]);
} as ViewModels.TreeNode);
const isDataNodeSelected = useSelectedNode
.getState()
.isDataNodeSelected(mockTab, "dbid", undefined, [ViewModels.CollectionTabKind.Documents]);
expect(isDataNodeSelected).toBeTruthy();
});
it("should select correct collection node (documents or graph node)", () => {
let subNodeKind = ViewModels.CollectionTabKind.Documents;
const explorer = mockContainer();
explorer.tabsManager.activeTab({
let activeTab = {
tabKind: subNodeKind,
} as TabsBase);
explorer.selectedNode(({
} as TabsBase;
useSelectedNode.getState().setSelectedNode({
nodeKind: "Collection",
rid: "collrid",
databaseId: "dbid",
id: ko.observable<string>("collid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as unknown) as ViewModels.TreeNode);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
let isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]);
} as ViewModels.TreeNode);
let isDataNodeSelected = useSelectedNode
.getState()
.isDataNodeSelected(activeTab, "dbid", "collid", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy();
subNodeKind = ViewModels.CollectionTabKind.Graph;
explorer.tabsManager.activeTab({
activeTab = {
tabKind: subNodeKind,
} as TabsBase);
explorer.selectedNode(({
} as TabsBase;
useSelectedNode.getState().setSelectedNode({
nodeKind: "Collection",
rid: "collrid",
databaseId: "dbid",
id: ko.observable<string>("collid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind),
} as unknown) as ViewModels.TreeNode);
isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [subNodeKind]);
} as ViewModels.TreeNode);
isDataNodeSelected = useSelectedNode.getState().isDataNodeSelected(activeTab, "dbid", "collid", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy();
});
it("should not select incorrect collection node (e.g. Settings)", () => {
const explorer = mockContainer();
explorer.selectedNode(({
useSelectedNode.getState().setSelectedNode({
nodeKind: "Collection",
rid: "collrid",
databaseId: "dbid",
id: ko.observable<string>("collid"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(ViewModels.CollectionTabKind.Documents),
} as unknown) as ViewModels.TreeNode);
explorer.tabsManager.activeTab({
} as ViewModels.TreeNode);
const activeTab = {
tabKind: ViewModels.CollectionTabKind.Documents,
} as TabsBase);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbid", "collid", [
ViewModels.CollectionTabKind.Settings,
]);
} as TabsBase;
const isDataNodeSelected = useSelectedNode
.getState()
.isDataNodeSelected(activeTab, "dbid", "collid", [ViewModels.CollectionTabKind.Settings]);
expect(isDataNodeSelected).toBeFalsy();
});
});

View File

@ -1,5 +1,4 @@
import { shallow } from "enzyme";
import * as ko from "knockout";
import React from "react";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
@ -208,13 +207,6 @@ const schema: DataModels.ISchema = {
],
};
const createMockContainer = (): Explorer => {
const mockContainer = new Explorer();
mockContainer.selectedNode = ko.observable<ViewModels.TreeNode>();
return mockContainer;
};
const createMockCollection = (): ViewModels.Collection => {
const mockCollection = {} as DataModels.Collection;
mockCollection._rid = "fakeRid";
@ -223,17 +215,13 @@ const createMockCollection = (): ViewModels.Collection => {
mockCollection.analyticalStorageTtl = 0;
mockCollection.schema = schema;
const mockCollectionVM: ViewModels.Collection = new Collection(
createMockContainer(),
"fakeDatabaseId",
mockCollection
);
const mockCollectionVM: ViewModels.Collection = new Collection(new Explorer(), "fakeDatabaseId", mockCollection);
return mockCollectionVM;
};
describe("Resource tree for schema", () => {
const mockContainer: Explorer = createMockContainer();
const mockContainer = new Explorer();
const resourceTree = new ResourceTreeAdapter(mockContainer);
it("should render", () => {

View File

@ -33,6 +33,7 @@ import { NotebookContentItem, NotebookContentItemType } from "../Notebook/Notebo
import { NotebookUtil } from "../Notebook/NotebookUtil";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
@ -54,7 +55,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
public constructor(private container: Explorer) {
this.parameters = ko.observable(Date.now());
this.container.selectedNode.subscribe((newValue: any) => this.triggerRender());
useSelectedNode.subscribe(() => this.triggerRender());
this.container.tabsManager.activeTab.subscribe((newValue: TabsBase) => this.triggerRender());
this.container.isNotebookEnabled.subscribe((newValue) => this.triggerRender());
@ -183,7 +184,8 @@ export class ResourceTreeAdapter implements ReactAdapter {
isExpanded: false,
className: "databaseHeader",
children: [],
isSelected: () => this.isDataNodeSelected(database.id()),
isSelected: () =>
useSelectedNode.getState().isDataNodeSelected(this.container.tabsManager.activeTab(), database.id()),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database.id()),
onClick: async (isExpanded) => {
// Rewritten version of expandCollapseDatabase():
@ -196,18 +198,22 @@ export class ResourceTreeAdapter implements ReactAdapter {
await database.expandDatabase();
}
databaseNode.isLoading = false;
database.selectDatabase();
useSelectedNode.getState().setSelectedNode(database);
useCommandBar.getState().setContextButtons([]);
this.container.tabsManager.refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id());
},
onContextMenuOpen: () => this.container.selectedNode(database),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database),
};
if (database.isDatabaseShared()) {
databaseNode.children.push({
label: "Scale",
isSelected: () =>
this.isDataNodeSelected(database.id(), undefined, [ViewModels.CollectionTabKind.DatabaseSettings]),
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), database.id(), undefined, [
ViewModels.CollectionTabKind.DatabaseSettings,
]),
onClick: database.onSettingsClick.bind(database),
});
}
@ -253,7 +259,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
},
isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.Documents,
ViewModels.CollectionTabKind.Graph,
]),
@ -265,7 +273,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection),
isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.SchemaAnalyzer,
]),
});
@ -276,7 +286,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
onClick: collection.onSettingsClick.bind(collection),
isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Settings]),
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.Settings,
]),
});
}
@ -302,7 +316,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: "Conflicts",
onClick: collection.onConflictsClick.bind(collection),
isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Conflicts]),
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.Conflicts,
]),
});
}
@ -315,7 +333,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection),
onClick: () => {
// Rewritten version of expandCollapseCollection
this.container.selectedNode(collection);
useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]);
this.container.tabsManager.refreshActiveTab(
(tab: TabsBase) =>
@ -329,8 +347,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
collection.loadTriggers();
}
},
isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id()),
onContextMenuOpen: () => this.container.selectedNode(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
};
}
@ -341,7 +362,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: sp.id(),
onClick: sp.open.bind(sp),
isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.StoredProcedures,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp),
@ -363,7 +386,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: udf.id(),
onClick: udf.open.bind(udf),
isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.UserDefinedFunctions,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(
@ -388,7 +413,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: trigger.id(),
onClick: trigger.open.bind(trigger),
isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]),
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.Triggers,
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger),
})),
onClick: () => {
@ -818,44 +847,4 @@ export class ResourceTreeAdapter implements ReactAdapter {
public triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
/**
* public for testing purposes
* @param databaseId
* @param collectionId
* @param subnodeKinds
*/
public isDataNodeSelected(
databaseId: string,
collectionId?: string,
subnodeKinds?: ViewModels.CollectionTabKind[]
): boolean {
if (!this.container.selectedNode || !this.container.selectedNode()) {
return false;
}
const selectedNode = this.container.selectedNode();
const isNodeSelected = collectionId
? (selectedNode as ViewModels.Collection).databaseId === databaseId && selectedNode.id() === collectionId
: selectedNode.id() === databaseId;
if (!isNodeSelected) {
return false;
}
if (subnodeKinds === undefined || !Array.isArray(subnodeKinds)) {
return true;
}
const activeTab = this.container.tabsManager.activeTab();
const selectedSubnodeKind = collectionId
? (selectedNode as ViewModels.Collection).selectedSubnodeKind()
: (selectedNode as ViewModels.Database).selectedSubnodeKind();
return (
activeTab &&
subnodeKinds.includes(activeTab.tabKind) &&
selectedSubnodeKind !== undefined &&
subnodeKinds.includes(selectedSubnodeKind)
);
}
}

View File

@ -11,7 +11,6 @@ import { ResourceTreeAdapterForResourceToken } from "./ResourceTreeAdapterForRes
const createMockContainer = (): Explorer => {
let mockContainer = {} as Explorer;
mockContainer.resourceTokenCollection = createMockCollection(mockContainer);
mockContainer.selectedNode = ko.observable<ViewModels.TreeNode>();
return mockContainer;
};

View File

@ -9,6 +9,7 @@ import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { useSelectedNode } from "../useSelectedNode";
export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
public parameters: ko.Observable<number>;
@ -18,7 +19,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
this.parameters = ko.observable(Date.now());
this.container.resourceTokenCollection.subscribe(() => this.triggerRender());
this.container.selectedNode.subscribe((newValue: any) => this.triggerRender());
useSelectedNode.subscribe(() => this.triggerRender());
this.container.tabsManager && this.container.tabsManager.activeTab.subscribe(() => this.triggerRender());
this.triggerRender();
@ -48,7 +49,11 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
},
isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), ViewModels.CollectionTabKind.Documents),
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.Documents,
]),
});
const collectionNode: TreeNode = {
@ -59,13 +64,16 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
className: "collectionHeader",
onClick: () => {
// Rewritten version of expandCollapseCollection
this.container.selectedNode(collection);
useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]);
this.container.tabsManager.refreshActiveTab(
(tab) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
);
},
isSelected: () => this.isDataNodeSelected(collection.databaseId, collection.id()),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(this.container.tabsManager.activeTab(), collection.databaseId, collection.id()),
};
return {
@ -75,35 +83,6 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
};
}
public isDataNodeSelected(
databaseId: string,
collectionId?: string,
subnodeKind?: ViewModels.CollectionTabKind
): boolean {
if (!this.container.selectedNode || !this.container.selectedNode()) {
return false;
}
const selectedNode = this.container.selectedNode();
const isNodeSelected = collectionId
? (selectedNode as ViewModels.Collection).databaseId === databaseId && selectedNode.id() === collectionId
: selectedNode.id() === databaseId;
if (!isNodeSelected) {
return false;
}
if (!subnodeKind) {
return true;
}
const activeTab = this.container.tabsManager.activeTab();
const selectedSubnodeKind = collectionId
? (selectedNode as ViewModels.Collection).selectedSubnodeKind()
: (selectedNode as ViewModels.Database).selectedSubnodeKind();
return activeTab && activeTab.tabKind === subnodeKind && selectedSubnodeKind === subnodeKind;
}
public triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}

View File

@ -11,6 +11,7 @@ import Explorer from "../Explorer";
import { getErrorMessage } from "../Tables/Utilities";
import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab";
import TabsBase from "../Tabs/TabsBase";
import { useSelectedNode } from "../useSelectedNode";
const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE
function sample(prefix) {
@ -87,7 +88,7 @@ export default class StoredProcedure {
}
public select() {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Stored procedure node",

View File

@ -7,6 +7,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer";
import TriggerTab from "../Tabs/TriggerTab";
import { useSelectedNode } from "../useSelectedNode";
export default class Trigger {
public nodeKind: string;
@ -32,7 +33,7 @@ export default class Trigger {
}
public select() {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Trigger node",

View File

@ -7,6 +7,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer";
import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab";
import { useSelectedNode } from "../useSelectedNode";
export default class UserDefinedFunction {
public nodeKind: string;
@ -82,7 +83,7 @@ export default class UserDefinedFunction {
};
public select() {
this.container.selectedNode(this);
useSelectedNode.getState().setSelectedNode(this);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "UDF item node",

View File

@ -0,0 +1,84 @@
import _ from "underscore";
import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
import TabsBase from "./Tabs/TabsBase";
import { useDatabases } from "./useDatabases";
export interface SelectedNodeState {
selectedNode: ViewModels.TreeNode;
setSelectedNode: (node: ViewModels.TreeNode) => void;
isDatabaseNodeOrNoneSelected: () => boolean;
findSelectedDatabase: () => ViewModels.Database;
findSelectedCollection: () => ViewModels.Collection;
isDataNodeSelected: (
activeTab: TabsBase,
databaseId: string,
collectionId?: string,
subnodeKinds?: ViewModels.CollectionTabKind[]
) => boolean;
}
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
selectedNode: undefined,
setSelectedNode: (node: ViewModels.TreeNode) => set({ selectedNode: node }),
isDatabaseNodeOrNoneSelected: (): boolean => {
const selectedNode = get().selectedNode;
return !selectedNode || selectedNode.nodeKind === "Database";
},
findSelectedDatabase: (): ViewModels.Database => {
const selectedNode = get().selectedNode;
if (!selectedNode) {
return undefined;
}
if (selectedNode.nodeKind === "Database") {
return _.find(
useDatabases.getState().databases,
(database: ViewModels.Database) => database.id() === selectedNode.id()
);
}
if (selectedNode.nodeKind === "Collection") {
return selectedNode.database;
}
return selectedNode.collection?.database;
},
findSelectedCollection: (): ViewModels.Collection => {
const selectedNode = get().selectedNode;
return (selectedNode.nodeKind === "Collection" ? selectedNode : selectedNode.collection) as ViewModels.Collection;
},
isDataNodeSelected: (
activeTab: TabsBase,
databaseId: string,
collectionId?: string,
subnodeKinds?: ViewModels.CollectionTabKind[]
): boolean => {
const selectedNode = get().selectedNode;
if (!selectedNode) {
return false;
}
const isNodeSelected = collectionId
? (selectedNode as ViewModels.Collection).databaseId === databaseId && selectedNode.id() === collectionId
: selectedNode.id() === databaseId;
if (!isNodeSelected) {
return false;
}
if (subnodeKinds === undefined || !Array.isArray(subnodeKinds)) {
return true;
}
const selectedSubnodeKind = collectionId
? (selectedNode as ViewModels.Collection).selectedSubnodeKind()
: (selectedNode as ViewModels.Database).selectedSubnodeKind();
return (
activeTab &&
subnodeKinds.includes(activeTab.tabKind) &&
selectedSubnodeKind !== undefined &&
subnodeKinds.includes(selectedSubnodeKind)
);
},
}));