From 96e6bba38b142aaee47b567f1f07645f2f7f9520 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 18 Jun 2021 11:25:08 -0700 Subject: [PATCH] Move databases to zustand (#898) --- src/Common/QueriesClient.ts | 3 +- .../SettingsComponent.test.tsx.snap | 4 - .../ContainerSampleGenerator.test.ts | 24 ++-- .../DataSamples/ContainerSampleGenerator.ts | 3 +- .../DataSamples/DataSamplesUtil.test.ts | 3 +- src/Explorer/DataSamples/DataSamplesUtil.ts | 3 +- src/Explorer/Explorer.test.tsx | 43 ------ src/Explorer/Explorer.tsx | 133 +++--------------- src/Explorer/Panes/AddCollectionPanel.tsx | 21 ++- .../AddDatabasePanel/AddDatabasePanel.tsx | 10 +- .../BrowseQueriesPane.test.tsx | 14 +- .../BrowseQueriesPane/BrowseQueriesPane.tsx | 4 +- .../BrowseQueriesPane.test.tsx.snap | 1 - .../CassandraAddCollectionPane.tsx | 9 +- .../DeleteCollectionConfirmationPane.test.tsx | 65 +++++---- .../DeleteCollectionConfirmationPane.tsx | 5 +- ...teCollectionConfirmationPane.test.tsx.snap | 1 - .../DeleteDatabaseConfirmationPanel.test.tsx | 43 +++--- .../Panes/DeleteDatabaseConfirmationPanel.tsx | 10 +- .../GitHubReposPanel.test.tsx.snap | 2 - .../SaveQueryPane/SaveQueryPane.test.tsx | 32 +++-- .../Panes/SaveQueryPane/SaveQueryPane.tsx | 11 +- .../StringInputPane.test.tsx.snap | 2 - ...eteDatabaseConfirmationPanel.test.tsx.snap | 62 ++------ src/Explorer/SplashScreen/SplashScreen.tsx | 5 +- src/Explorer/Tree/Collection.ts | 3 +- src/Explorer/Tree/ResourceTokenCollection.ts | 3 +- src/Explorer/Tree/ResourceTreeAdapter.tsx | 26 ++-- src/Explorer/useDatabases.ts | 113 +++++++++++++++ src/RouteHandlers/TabRouteHandler.ts | 8 +- src/Shared/AddCollectionUtility.test.ts | 64 --------- src/Shared/AddCollectionUtility.ts | 23 --- src/hooks/useKnockoutExplorer.ts | 3 +- 33 files changed, 310 insertions(+), 446 deletions(-) delete mode 100644 src/Explorer/Explorer.test.tsx create mode 100644 src/Explorer/useDatabases.ts delete mode 100644 src/Shared/AddCollectionUtility.test.ts delete mode 100644 src/Shared/AddCollectionUtility.ts diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index dfb31a2f6..534d12879 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -4,6 +4,7 @@ import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "../Explorer/Explorer"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { useDatabases } from "../Explorer/useDatabases"; import { userContext } from "../UserContext"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; @@ -176,7 +177,7 @@ export class QueriesClient { private findQueriesCollection(): ViewModels.Collection { const queriesDatabase: ViewModels.Database = _.find( - this.container.databases(), + useDatabases.getState().databases, (database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName ); if (!queriesDatabase) { diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 99f63dd81..42aa68497 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -30,8 +30,6 @@ exports[`SettingsComponent renders 1`] = ` "container": Explorer { "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], - "canSaveQueries": [Function], - "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isNotebookEnabled": [Function], @@ -124,8 +122,6 @@ exports[`SettingsComponent renders 1`] = ` "container": Explorer { "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], - "canSaveQueries": [Function], - "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isNotebookEnabled": [Function], diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts index b9932656a..aca891f24 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts @@ -2,22 +2,22 @@ jest.mock("../Graph/GraphExplorerComponent/GremlinClient"); jest.mock("../../Common/dataAccess/createCollection"); jest.mock("../../Common/dataAccess/createDocument"); import * as ko from "knockout"; -import Q from "q"; import { createDocument } from "../../Common/dataAccess/createDocument"; import { DatabaseAccount } from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { updateUserContext } from "../../UserContext"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; describe("ContainerSampleGenerator", () => { - const createExplorerStub = (database: ViewModels.Database): Explorer => { - const explorerStub = {} as Explorer; - explorerStub.databases = ko.observableArray([database]); - explorerStub.findDatabaseWithId = () => database; - explorerStub.refreshAllDatabases = () => Q.resolve(); - return explorerStub; - }; + let explorerStub: Explorer; + + beforeAll(() => { + explorerStub = { + refreshAllDatabases: () => {}, + } as Explorer; + }); beforeEach(() => { (createDocument as jest.Mock).mockResolvedValue(undefined); @@ -59,8 +59,7 @@ describe("ContainerSampleGenerator", () => { loadCollections: () => {}, } as ViewModels.Database; database.findCollectionWithId = () => collection; - - const explorerStub = createExplorerStub(database); + useDatabases.getState().addDatabases([database]); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); generator.setData(sampleData); @@ -108,8 +107,8 @@ describe("ContainerSampleGenerator", () => { } as ViewModels.Database; database.findCollectionWithId = () => collection; collection.databaseId = database.id(); + useDatabases.getState().addDatabases([database]); - const explorerStub = createExplorerStub(database); updateUserContext({ databaseAccount: { properties: { @@ -126,7 +125,6 @@ describe("ContainerSampleGenerator", () => { it("should not create any sample for Mongo API account", async () => { const experience = "Sample generation not supported for this API Mongo"; - const explorerStub = createExplorerStub(undefined); updateUserContext({ databaseAccount: { properties: { @@ -141,7 +139,6 @@ describe("ContainerSampleGenerator", () => { it("should not create any sample for Table API account", async () => { const experience = "Sample generation not supported for this API Tables"; - const explorerStub = createExplorerStub(undefined); updateUserContext({ databaseAccount: { properties: { @@ -163,7 +160,6 @@ describe("ContainerSampleGenerator", () => { }, } as DatabaseAccount, }); - const explorerStub = createExplorerStub(undefined); // Rejects with error that contains experience await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); }); diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.ts index dd9a4adb9..44906151d 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.ts @@ -7,6 +7,7 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils" import GraphTab from ".././Tabs/GraphTab"; import Explorer from "../Explorer"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; +import { useDatabases } from "../useDatabases"; interface SampleDataFile extends DataModels.CreateCollectionParams { data: any[]; @@ -59,7 +60,7 @@ export class ContainerSampleGenerator { await createCollection(createRequest); await this.container.refreshAllDatabases(); - const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId); + const database = useDatabases.getState().findDatabaseWithId(this.sampleDataFile.databaseId); if (!database) { return undefined; } diff --git a/src/Explorer/DataSamples/DataSamplesUtil.test.ts b/src/Explorer/DataSamples/DataSamplesUtil.test.ts index 9a35158f5..f8ff6f8e5 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.test.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.test.ts @@ -2,6 +2,7 @@ import * as ko from "knockout"; import * as sinon from "sinon"; import { Collection, Database } from "../../Contracts/ViewModels"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { DataSamplesUtil } from "./DataSamplesUtil"; @@ -16,8 +17,8 @@ describe("DataSampleUtils", () => { collections: ko.observableArray([collection]), } as Database; const explorer = {} as Explorer; - explorer.databases = ko.observableArray([database]); explorer.showOkModalDialog = () => {}; + useDatabases.getState().addDatabases([database]); const dataSamplesUtil = new DataSamplesUtil(explorer); const fakeGenerator = sinon.createStubInstance(ContainerSampleGenerator as any); diff --git a/src/Explorer/DataSamples/DataSamplesUtil.ts b/src/Explorer/DataSamples/DataSamplesUtil.ts index 63b35cfff..4007608c0 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.ts @@ -2,6 +2,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { userContext } from "../../UserContext"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; export class DataSamplesUtil { @@ -17,7 +18,7 @@ export class DataSamplesUtil { const databaseName = generator.getDatabaseId(); const containerName = generator.getCollectionId(); - if (this.hasContainer(databaseName, containerName, this.container.databases())) { + if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) { const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); logConsoleError(msg); diff --git a/src/Explorer/Explorer.test.tsx b/src/Explorer/Explorer.test.tsx deleted file mode 100644 index ef595252a..000000000 --- a/src/Explorer/Explorer.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -jest.mock("./../Common/dataAccess/deleteDatabase"); -jest.mock("./../Shared/Telemetry/TelemetryProcessor"); -import * as ko from "knockout"; -import { deleteDatabase } from "./../Common/dataAccess/deleteDatabase"; -import * as ViewModels from "./../Contracts/ViewModels"; -import Explorer from "./Explorer"; - -describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => { - let explorer: Explorer; - beforeAll(() => { - (deleteDatabase as jest.Mock).mockResolvedValue(undefined); - }); - - beforeEach(() => { - explorer = new Explorer(); - }); - - it("should be true if only 1 database", () => { - const database = {} as ViewModels.Database; - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastDatabase()).toBe(true); - }); - - it("should be false if only 2 databases", () => { - const database = {} as ViewModels.Database; - const database2 = {} as ViewModels.Database; - explorer.databases = ko.observableArray([database, database2]); - expect(explorer.isLastDatabase()).toBe(false); - }); - - it("should be false if not last empty database", () => { - const database = {} as ViewModels.Database; - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastNonEmptyDatabase()).toBe(false); - }); - - it("should be true if last non empty database", () => { - const database = {} as ViewModels.Database; - database.collections = ko.observableArray([{} as ViewModels.Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastNonEmptyDatabase()).toBe(true); - }); -}); diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 108f5df35..bb7ca660c 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -69,6 +69,7 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import StoredProcedure from "./Tree/StoredProcedure"; +import { useDatabases } from "./useDatabases"; BindingHandlersRegisterer.registerBindingHandlers(); // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import @@ -81,12 +82,10 @@ export interface ExplorerParams { export default class Explorer { public isFixedCollectionWithSharedThroughputSupported: ko.Computed; public isAccountReady: ko.Observable; - public canSaveQueries: ko.Computed; public queriesClient: QueriesClient; public tableDataClient: TableDataClient; // Resource Tree - public databases: ko.ObservableArray; public selectedDatabaseId: ko.Computed; public selectedCollectionId: ko.Computed; public selectedNode: ko.Observable; @@ -168,26 +167,6 @@ export default class Explorer { this.resourceTokenCollection = ko.observable(); this.isSchemaEnabled = ko.computed(() => userContext.features.enableSchema); - this.databases = ko.observableArray(); - this.canSaveQueries = ko.computed(() => { - const savedQueriesDatabase: ViewModels.Database = _.find( - this.databases(), - (database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName - ); - if (!savedQueriesDatabase) { - return false; - } - const savedQueriesCollection: ViewModels.Collection = - savedQueriesDatabase && - _.find( - savedQueriesDatabase.collections(), - (collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName - ); - if (!savedQueriesCollection) { - return false; - } - return true; - }); this.selectedNode = ko.observable(); this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { // Make sure switching tabs restores tabs display @@ -641,34 +620,14 @@ export default class Explorer { return null; } if (this.selectedNode().nodeKind === "Database") { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id()); + return _.find( + useDatabases.getState().databases, + (database: ViewModels.Database) => database.id() === this.selectedNode().id() + ); } return this.findSelectedCollection().database; } - public findDatabaseWithId(databaseId: string): ViewModels.Database { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === databaseId); - } - - public isLastNonEmptyDatabase(): boolean { - if ( - this.isLastDatabase() && - this.databases()[0] && - this.databases()[0].collections && - this.databases()[0].collections().length > 0 - ) { - return true; - } - return false; - } - - public isLastDatabase(): boolean { - if (this.databases().length > 1) { - return false; - } - return true; - } - public isSelectedDatabaseShared(): boolean { const database = this.findSelectedDatabase(); if (!!database) { @@ -691,10 +650,11 @@ export default class Explorer { let loadCollectionPromises: Q.Promise[] = []; // If the user has a lot of databases, only load expanded databases. + const databases = useDatabases.getState().databases; const databasesToLoad = - this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand - ? this.databases() - : this.databases().filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName); + databases.length <= Explorer.MaxNbDatabasesToAutoExpand + ? databases + : databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName); const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { dataExplorerArea: Constants.Areas.ResourceTree, @@ -739,37 +699,16 @@ export default class Explorer { } } - public findCollection(databaseId: string, collectionId: string): ViewModels.Collection { - const database: ViewModels.Database = this.databases().find( - (database: ViewModels.Database) => database.id() === databaseId - ); - return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId); - } - - public isLastCollection(): boolean { - let collectionCount = 0; - if (this.databases().length == 0) { - return false; - } - for (let i = 0; i < this.databases().length; i++) { - const database = this.databases()[i]; - collectionCount += database.collections().length; - if (collectionCount > 1) { - return false; - } - } - return true; - } - private getDeltaDatabases( updatedDatabaseList: DataModels.Database[] ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[]; } { + const databases = useDatabases.getState().databases; const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const databaseExists = _.some( - this.databases(), + databases, (existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id ); return !databaseExists; @@ -779,7 +718,7 @@ export default class Explorer { ); let databasesToDelete: ViewModels.Database[] = []; - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { + ko.utils.arrayForEach(databases, (database: ViewModels.Database) => { const databasePresentInUpdatedList = _.some( updatedDatabaseList, (db: DataModels.Database) => db.id === database.id() @@ -793,24 +732,12 @@ export default class Explorer { } private addDatabasesToList(databases: ViewModels.Database[]): void { - this.databases( - this.databases() - .concat(databases) - .sort((database1, database2) => database1.id().localeCompare(database2.id())) - ); + useDatabases.getState().addDatabases(databases); } private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void { - const databasesToKeep: ViewModels.Database[] = []; - - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { - const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id); - if (!shouldRemoveDatabase) { - databasesToKeep.push(database); - } - }); - - this.databases(databasesToKeep); + const deleteDatabase = useDatabases.getState().deleteDatabase; + databasesToRemove.forEach((database) => deleteDatabase(database)); } public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { @@ -1414,34 +1341,6 @@ export default class Explorer { } } - public async loadDatabaseOffers(): Promise { - await Promise.all( - this.databases()?.map(async (database: ViewModels.Database) => { - await database.loadOffer(); - }) - ); - } - - public isFirstResourceCreated(): boolean { - const databases: ViewModels.Database[] = this.databases(); - - if (!databases || databases.length === 0) { - return false; - } - - return databases.some((database) => { - // user has created at least one collection - if (database.collections()?.length > 0) { - return true; - } - // user has created a database with shared throughput - if (database.offer()) { - return true; - } - // use has created an empty database without shared throughput - return false; - }); - } public openDeleteCollectionConfirmationPane(): void { useSidePanel .getState() @@ -1466,7 +1365,7 @@ export default class Explorer { } public async openAddCollectionPanel(databaseId?: string): Promise { - await this.loadDatabaseOffers(); + await useDatabases.getState().loadDatabaseOffers(); useSidePanel .getState() .openSidePanel("New " + getCollectionName(), ); diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index aa98e4113..a946c1091 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -31,6 +31,7 @@ import { getUpsellMessage } from "../../Utils/PricingUtils"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { PanelFooterComponent } from "./PanelFooterComponent"; import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent"; import { PanelLoadingScreen } from "./PanelLoadingScreen"; @@ -125,6 +126,8 @@ export class AddCollectionPanel extends React.Component {this.state.errorMessage && ( @@ -137,7 +140,7 @@ export class AddCollectionPanel extends React.Component (this.newDatabaseThroughput = throughput)} @@ -469,9 +470,7 @@ export class AddCollectionPanel extends React.Component (this.collectionThroughput = throughput)} @@ -680,7 +679,7 @@ export class AddCollectionPanel extends React.Component ({ + return useDatabases.getState().databases?.map((database) => ({ key: database.id(), text: database.id(), })); @@ -772,9 +771,9 @@ export class AddCollectionPanel extends React.Component database.id() === this.state.selectedDatabaseId); + const selectedDatabase = useDatabases + .getState() + .databases?.find((database) => database.id() === this.state.selectedDatabaseId); return !!selectedDatabase?.offer(); } diff --git a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx index 9bd37368b..3ee59bd30 100644 --- a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx +++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx @@ -16,6 +16,7 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; import { getUpsellMessage } from "../../../Utils/PricingUtils"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { getTextFieldStyles } from "../PanelStyles"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; @@ -172,7 +173,12 @@ export const AddDatabasePanel: FunctionComponent = ({ {!formErrors && isFreeTierAccount && ( = ({ {!isServerlessAccount() && databaseCreateNewShared && ( (throughput = newThroughput)} diff --git a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx index e052a6616..834bbed18 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx +++ b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx @@ -1,14 +1,16 @@ import { mount } from "enzyme"; import * as ko from "knockout"; import React from "react"; +import { SavedQueries } from "../../../Common/Constants"; import { QueriesClient } from "../../../Common/QueriesClient"; import { Query } from "../../../Contracts/DataModels"; +import { Collection, Database } from "../../../Contracts/ViewModels"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { BrowseQueriesPane } from "./BrowseQueriesPane"; describe("Browse queries panel", () => { const fakeExplorer = {} as Explorer; - fakeExplorer.canSaveQueries = ko.computed(() => true); const fakeClientQuery = {} as QueriesClient; const fakeQueryData = [] as Query[]; fakeClientQuery.getQueries = async () => fakeQueryData; @@ -17,6 +19,16 @@ describe("Browse queries panel", () => { explorer: fakeExplorer, closePanel: (): void => undefined, }; + useDatabases.getState().addDatabases([ + { + id: ko.observable(SavedQueries.DatabaseName), + collections: ko.observableArray([ + { + id: ko.observable(SavedQueries.CollectionName), + } as Collection, + ]), + } as Database, + ]); it("Should render Default properly", () => { const wrapper = mount(); diff --git a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx index 13ae70e44..9624b4539 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx +++ b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx @@ -13,6 +13,7 @@ import { } from "../../Controls/QueriesGridReactComponent/QueriesGridComponent"; import Explorer from "../../Explorer"; import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab"; +import { useDatabases } from "../../useDatabases"; interface BrowseQueriesPaneProps { explorer: Explorer; @@ -45,12 +46,13 @@ export const BrowseQueriesPane: FunctionComponent = ({ }); closeSidePanel(); }; + const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled); const props: QueriesGridComponentProps = { queriesClient: explorer.queriesClient, onQuerySelect: loadSavedQuery, containerVisible: true, - saveQueryEnabled: explorer.canSaveQueries(), + saveQueryEnabled: isSaveQueryEnabled(), }; return ( diff --git a/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap b/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap index eed894482..5fcb72a63 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap +++ b/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap @@ -5,7 +5,6 @@ exports[`Browse queries panel Should render Default properly 1`] = ` closePanel={[Function]} explorer={ Object { - "canSaveQueries": [Function], "queriesClient": Object { "getQueries": [Function], }, diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx index 3f829c191..7d0703336 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -12,6 +12,7 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import Explorer from "../../Explorer"; import { CassandraAPIDataClient } from "../../Tables/TableDataClient"; +import { useDatabases } from "../../useDatabases"; import { getTextFieldStyles } from "../PanelStyles"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; @@ -236,7 +237,7 @@ export const CassandraAddCollectionPane: FunctionComponent ({ + options={useDatabases.getState().databases?.map((keyspace) => ({ key: keyspace.id(), text: keyspace.id(), data: { @@ -253,7 +254,9 @@ export const CassandraAddCollectionPane: FunctionComponent (newKeySpaceThroughput = throughput)} @@ -324,7 +327,7 @@ export const CassandraAddCollectionPane: FunctionComponent (tableThroughput = throughput)} diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx index c75f998e3..62aeade73 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx @@ -11,44 +11,42 @@ import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryCons import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { updateUserContext } from "../../../UserContext"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane"; describe("Delete Collection Confirmation Pane", () => { - describe("Explorer.isLastCollection()", () => { - let explorer: Explorer; - - beforeEach(() => { - explorer = new Explorer(); - }); + describe("useDatabases.isLastCollection()", () => { + beforeAll(() => useDatabases.getState().clearDatabases()); + afterEach(() => useDatabases.getState().clearDatabases()); it("should be true if 1 database and 1 collection", () => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastCollection()).toBe(true); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + useDatabases.getState().addDatabases([database]); + expect(useDatabases.getState().isLastCollection()).toBe(true); }); it("should be false if if 1 database and 2 collection", () => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection, {} as Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastCollection()).toBe(false); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([ + { id: ko.observable("coll1") } as Collection, + { id: ko.observable("coll2") } as Collection, + ]); + useDatabases.getState().addDatabases([database]); + expect(useDatabases.getState().isLastCollection()).toBe(false); }); it("should be false if 2 database and 1 collection each", () => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection]); - const database2 = {} as Database; - database2.collections = ko.observableArray([{} as Collection]); - explorer.databases = ko.observableArray([database, database2]); - expect(explorer.isLastCollection()).toBe(false); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("coll1") } as Collection]); + const database2 = { id: ko.observable("testDB2") } as Database; + database2.collections = ko.observableArray([{ id: ko.observable("coll2") } as Collection]); + useDatabases.getState().addDatabases([database, database2]); + expect(useDatabases.getState().isLastCollection()).toBe(false); }); it("should be false if 0 databases", () => { - const database = {} as Database; - explorer.databases = ko.observableArray(); - database.collections = ko.observableArray(); - expect(explorer.isLastCollection()).toBe(false); + expect(useDatabases.getState().isLastCollection()).toBe(false); }); }); @@ -56,7 +54,6 @@ 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.isLastCollection = () => true; fakeExplorer.isSelectedDatabaseShared = () => false; const props = { @@ -65,15 +62,15 @@ describe("Delete Collection Confirmation Pane", () => { collectionName: "container", }; const wrapper = shallow(); - expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); - - props.explorer.isLastCollection = () => true; - props.explorer.isSelectedDatabaseShared = () => true; - wrapper.setProps(props); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); - props.explorer.isLastCollection = () => false; - props.explorer.isSelectedDatabaseShared = () => false; + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + useDatabases.getState().addDatabases([database]); + wrapper.setProps(props); + expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); + + props.explorer.isSelectedDatabaseShared = () => true; wrapper.setProps(props); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); }); @@ -94,8 +91,10 @@ describe("Delete Collection Confirmation Pane", () => { fakeExplorer.selectedCollectionId = ko.computed(() => selectedCollectionId); fakeExplorer.selectedNode = ko.observable(); fakeExplorer.refreshAllDatabases = () => undefined; - fakeExplorer.isLastCollection = () => true; fakeExplorer.isSelectedDatabaseShared = () => false; + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + useDatabases.getState().addDatabases([database]); beforeAll(() => { updateUserContext({ diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx index 8fad674bd..bec5db8a0 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx @@ -13,7 +13,9 @@ import { userContext } from "../../../UserContext"; import { getCollectionName } from "../../../Utils/APITypeUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; + export interface DeleteCollectionConfirmationPaneProps { explorer: Explorer; } @@ -22,13 +24,14 @@ export const DeleteCollectionConfirmationPane: FunctionComponent { const closeSidePanel = useSidePanel((state) => state.closeSidePanel); + const isLastCollection = useDatabases((state) => state.isLastCollection); const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState(""); const [inputCollectionName, setInputCollectionName] = useState(""); const [formError, setFormError] = useState(""); const [isExecuting, setIsExecuting] = useState(false); const shouldRecordFeedback = (): boolean => { - return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared(); + return isLastCollection() && !explorer.isSelectedDatabaseShared(); }; const collectionName = getCollectionName().toLocaleLowerCase(); const paneTitle = "Delete " + collectionName; diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap index 59e62296b..af75f4fe6 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -7,7 +7,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect explorer={ Object { "findSelectedCollection": [Function], - "isLastCollection": [Function], "isSelectedDatabaseShared": [Function], "refreshAllDatabases": [Function], "selectedCollectionId": [Function], diff --git a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.test.tsx b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.test.tsx index f7082c8ff..b3195252d 100644 --- a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.test.tsx +++ b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.test.tsx @@ -11,19 +11,20 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { updateUserContext } from "../../UserContext"; import Explorer from "../Explorer"; +import { TabsManager } from "../Tabs/TabsManager"; +import { useDatabases } from "../useDatabases"; 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 = new Explorer(); + const fakeExplorer = {} as Explorer; fakeExplorer.refreshAllDatabases = () => undefined; - fakeExplorer.isLastCollection = () => true; fakeExplorer.isSelectedDatabaseShared = () => false; const database = {} as Database; - database.collections = ko.observableArray([{} as Collection]); - database.id = ko.observable("testDatabse"); + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + database.id = ko.observable("testDatabase"); const props = { explorer: fakeExplorer, @@ -33,29 +34,26 @@ describe("Delete Database Confirmation Pane", () => { }; const wrapper = shallow(); - props.explorer.isLastNonEmptyDatabase = () => true; + expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false); + + useDatabases.getState().addDatabases([database]); + wrapper.setProps(props); expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true); - - props.explorer.isLastNonEmptyDatabase = () => false; - props.explorer.isLastDatabase = () => false; - wrapper.setProps(props); - expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false); - - props.explorer.isLastNonEmptyDatabase = () => false; - props.explorer.isLastDatabase = () => true; - props.explorer.isSelectedDatabaseShared = () => false; - wrapper.setProps(props); - expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false); + useDatabases.getState().clearDatabases(); }); }); describe("submit()", () => { const selectedDatabaseId = "testDatabse"; - const fakeExplorer = new Explorer(); + const database = { id: ko.observable("testDatabase") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + database.id = ko.observable(selectedDatabaseId); + const fakeExplorer = {} as Explorer; fakeExplorer.refreshAllDatabases = () => undefined; - fakeExplorer.isLastCollection = () => true; fakeExplorer.isSelectedDatabaseShared = () => false; + fakeExplorer.tabsManager = new TabsManager(); + fakeExplorer.selectedNode = ko.observable(); let wrapper: ReactWrapper; beforeAll(() => { @@ -71,13 +69,10 @@ describe("Delete Database Confirmation Pane", () => { }); (deleteDatabase as jest.Mock).mockResolvedValue(undefined); (TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined); + useDatabases.getState().addDatabases([database]); }); beforeEach(() => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection]); - database.id = ko.observable(selectedDatabaseId); - const props = { explorer: fakeExplorer, closePanel: (): void => undefined, @@ -86,10 +81,10 @@ describe("Delete Database Confirmation Pane", () => { }; wrapper = mount(); - props.explorer.isLastNonEmptyDatabase = () => true; - wrapper.setProps(props); }); + afterAll(() => useDatabases.getState().clearDatabases()); + it("Should call delete database", () => { expect(wrapper).toMatchSnapshot(); expect(wrapper.exists("#confirmDatabaseId")).toBe(true); diff --git a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx index 56a9982b6..64cb7a211 100644 --- a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx +++ b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx @@ -13,6 +13,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm"; @@ -26,6 +27,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent { const closeSidePanel = useSidePanel((state) => state.closeSidePanel); + const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [formError, setFormError] = useState(""); @@ -70,7 +72,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent { - return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared()); - }; - const props: RightPaneFormProps = { formError, isExecuting: isLoading, @@ -134,7 +132,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent - {shouldRecordFeedback() && ( + {isLastNonEmptyDatabase() && (
Help us improve Azure Cosmos DB! diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 047d8ae26..7ad772ba0 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -19,8 +19,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "container": Explorer { "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], - "canSaveQueries": [Function], - "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isNotebookEnabled": [Function], diff --git a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.test.tsx b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.test.tsx index 8edacc4a0..635483a49 100644 --- a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.test.tsx +++ b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.test.tsx @@ -1,32 +1,38 @@ import { shallow } from "enzyme"; import * as ko from "knockout"; import React from "react"; +import { SavedQueries } from "../../../Common/Constants"; +import { Collection, Database } from "../../../Contracts/ViewModels"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { SaveQueryPane } from "./SaveQueryPane"; describe("Save Query Pane", () => { const fakeExplorer = {} as Explorer; - fakeExplorer.canSaveQueries = ko.computed(() => true); const props = { explorer: fakeExplorer, closePanel: (): void => undefined, }; - const wrapper = shallow(); - - it("should return true if can save Queries else false", () => { - fakeExplorer.canSaveQueries = ko.computed(() => true); - wrapper.setProps(props); - expect(wrapper.exists("#saveQueryInput")).toBe(true); - - fakeExplorer.canSaveQueries = ko.computed(() => false); - wrapper.setProps(props); - expect(wrapper.exists("#saveQueryInput")).toBe(false); - }); - it("should render Default properly", () => { const wrapper = shallow(); + expect(wrapper.exists("#saveQueryInput")).toBe(false); expect(wrapper).toMatchSnapshot(); }); + + it("should return true if can save Queries else false", () => { + useDatabases.getState().addDatabases([ + { + id: ko.observable(SavedQueries.DatabaseName), + collections: ko.observableArray([ + { + id: ko.observable(SavedQueries.CollectionName), + } as Collection, + ]), + } as Database, + ]); + const wrapper = shallow(); + expect(wrapper.exists("#saveQueryInput")).toBe(true); + }); }); diff --git a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx index ff5089d88..f852b09c2 100644 --- a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx +++ b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx @@ -10,6 +10,7 @@ import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetr import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab"; +import { useDatabases } from "../../useDatabases"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; interface SaveQueryPaneProps { @@ -24,11 +25,11 @@ export const SaveQueryPane: FunctionComponent = ({ explorer const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`; const title = "Save Query"; - const { canSaveQueries } = explorer; + const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled); const submit = async (): Promise => { setFormError(""); - if (!canSaveQueries()) { + if (!isSaveQueryEnabled()) { setFormError("Cannot save query"); logConsoleError("Failed to save query: account not setup to save queries"); } @@ -129,16 +130,16 @@ export const SaveQueryPane: FunctionComponent = ({ explorer const props: RightPaneFormProps = { formError: formError, isExecuting: isLoading, - submitButtonText: canSaveQueries() ? "Save" : "Complete setup", + submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup", onSubmit: () => { - canSaveQueries() ? submit() : setupQueries(); + isSaveQueryEnabled() ? submit() : setupQueries(); }, }; return (
- {!canSaveQueries() ? ( + {!isSaveQueryEnabled() ? ( {setupSaveQueriesText} ) : ( Help us improve Azure Cosmos DB! @@ -759,7 +719,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database variant="small" > What is the reason why you are deleting this database? @@ -1067,11 +1027,11 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database className="ms-TextField-wrapper" >