Move databases to zustand (#898)

This commit is contained in:
victor-meng 2021-06-18 11:25:08 -07:00 committed by GitHub
parent c9fa44f6f4
commit 96e6bba38b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 310 additions and 446 deletions

View File

@ -4,6 +4,7 @@ import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { useDatabases } from "../Explorer/useDatabases";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
@ -176,7 +177,7 @@ export class QueriesClient {
private findQueriesCollection(): ViewModels.Collection { private findQueriesCollection(): ViewModels.Collection {
const queriesDatabase: ViewModels.Database = _.find( const queriesDatabase: ViewModels.Database = _.find(
this.container.databases(), useDatabases.getState().databases,
(database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName (database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName
); );
if (!queriesDatabase) { if (!queriesDatabase) {

View File

@ -30,8 +30,6 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"databases": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
@ -124,8 +122,6 @@ exports[`SettingsComponent renders 1`] = `
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"databases": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],

View File

@ -2,22 +2,22 @@ jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection"); jest.mock("../../Common/dataAccess/createCollection");
jest.mock("../../Common/dataAccess/createDocument"); jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
import { DatabaseAccount } from "../../Contracts/DataModels"; import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
describe("ContainerSampleGenerator", () => { describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => { let explorerStub: Explorer;
const explorerStub = {} as Explorer;
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]); beforeAll(() => {
explorerStub.findDatabaseWithId = () => database; explorerStub = {
explorerStub.refreshAllDatabases = () => Q.resolve(); refreshAllDatabases: () => {},
return explorerStub; } as Explorer;
}; });
beforeEach(() => { beforeEach(() => {
(createDocument as jest.Mock).mockResolvedValue(undefined); (createDocument as jest.Mock).mockResolvedValue(undefined);
@ -59,8 +59,7 @@ describe("ContainerSampleGenerator", () => {
loadCollections: () => {}, loadCollections: () => {},
} as ViewModels.Database; } as ViewModels.Database;
database.findCollectionWithId = () => collection; database.findCollectionWithId = () => collection;
useDatabases.getState().addDatabases([database]);
const explorerStub = createExplorerStub(database);
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData); generator.setData(sampleData);
@ -108,8 +107,8 @@ describe("ContainerSampleGenerator", () => {
} as ViewModels.Database; } as ViewModels.Database;
database.findCollectionWithId = () => collection; database.findCollectionWithId = () => collection;
collection.databaseId = database.id(); collection.databaseId = database.id();
useDatabases.getState().addDatabases([database]);
const explorerStub = createExplorerStub(database);
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {
properties: { properties: {
@ -126,7 +125,6 @@ describe("ContainerSampleGenerator", () => {
it("should not create any sample for Mongo API account", async () => { it("should not create any sample for Mongo API account", async () => {
const experience = "Sample generation not supported for this API Mongo"; const experience = "Sample generation not supported for this API Mongo";
const explorerStub = createExplorerStub(undefined);
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {
properties: { properties: {
@ -141,7 +139,6 @@ describe("ContainerSampleGenerator", () => {
it("should not create any sample for Table API account", async () => { it("should not create any sample for Table API account", async () => {
const experience = "Sample generation not supported for this API Tables"; const experience = "Sample generation not supported for this API Tables";
const explorerStub = createExplorerStub(undefined);
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {
properties: { properties: {
@ -163,7 +160,6 @@ describe("ContainerSampleGenerator", () => {
}, },
} as DatabaseAccount, } as DatabaseAccount,
}); });
const explorerStub = createExplorerStub(undefined);
// Rejects with error that contains experience // Rejects with error that contains experience
await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
}); });

View File

@ -7,6 +7,7 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
import GraphTab from ".././Tabs/GraphTab"; import GraphTab from ".././Tabs/GraphTab";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import { useDatabases } from "../useDatabases";
interface SampleDataFile extends DataModels.CreateCollectionParams { interface SampleDataFile extends DataModels.CreateCollectionParams {
data: any[]; data: any[];
@ -59,7 +60,7 @@ export class ContainerSampleGenerator {
await createCollection(createRequest); await createCollection(createRequest);
await this.container.refreshAllDatabases(); await this.container.refreshAllDatabases();
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId); const database = useDatabases.getState().findDatabaseWithId(this.sampleDataFile.databaseId);
if (!database) { if (!database) {
return undefined; return undefined;
} }

View File

@ -2,6 +2,7 @@ import * as ko from "knockout";
import * as sinon from "sinon"; import * as sinon from "sinon";
import { Collection, Database } from "../../Contracts/ViewModels"; import { Collection, Database } from "../../Contracts/ViewModels";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { DataSamplesUtil } from "./DataSamplesUtil"; import { DataSamplesUtil } from "./DataSamplesUtil";
@ -16,8 +17,8 @@ describe("DataSampleUtils", () => {
collections: ko.observableArray<Collection>([collection]), collections: ko.observableArray<Collection>([collection]),
} as Database; } as Database;
const explorer = {} as Explorer; const explorer = {} as Explorer;
explorer.databases = ko.observableArray<Database>([database]);
explorer.showOkModalDialog = () => {}; explorer.showOkModalDialog = () => {};
useDatabases.getState().addDatabases([database]);
const dataSamplesUtil = new DataSamplesUtil(explorer); const dataSamplesUtil = new DataSamplesUtil(explorer);
const fakeGenerator = sinon.createStubInstance<ContainerSampleGenerator>(ContainerSampleGenerator as any); const fakeGenerator = sinon.createStubInstance<ContainerSampleGenerator>(ContainerSampleGenerator as any);

View File

@ -2,6 +2,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
export class DataSamplesUtil { export class DataSamplesUtil {
@ -17,7 +18,7 @@ export class DataSamplesUtil {
const databaseName = generator.getDatabaseId(); const databaseName = generator.getDatabaseId();
const containerName = generator.getCollectionId(); 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.`; const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
logConsoleError(msg); logConsoleError(msg);

View File

@ -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<ViewModels.Database>([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<ViewModels.Database>([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<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(false);
});
it("should be true if last non empty database", () => {
const database = {} as ViewModels.Database;
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
expect(explorer.isLastNonEmptyDatabase()).toBe(true);
});
});

View File

@ -69,6 +69,7 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import { useDatabases } from "./useDatabases";
BindingHandlersRegisterer.registerBindingHandlers(); BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@ -81,12 +82,10 @@ export interface ExplorerParams {
export default class Explorer { export default class Explorer {
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>; public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public isAccountReady: ko.Observable<boolean>; public isAccountReady: ko.Observable<boolean>;
public canSaveQueries: ko.Computed<boolean>;
public queriesClient: QueriesClient; public queriesClient: QueriesClient;
public tableDataClient: TableDataClient; public tableDataClient: TableDataClient;
// Resource Tree // Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>;
public selectedDatabaseId: ko.Computed<string>; public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>; public selectedCollectionId: ko.Computed<string>;
public selectedNode: ko.Observable<ViewModels.TreeNode>; public selectedNode: ko.Observable<ViewModels.TreeNode>;
@ -168,26 +167,6 @@ export default class Explorer {
this.resourceTokenCollection = ko.observable<ViewModels.CollectionBase>(); this.resourceTokenCollection = ko.observable<ViewModels.CollectionBase>();
this.isSchemaEnabled = ko.computed<boolean>(() => userContext.features.enableSchema); this.isSchemaEnabled = ko.computed<boolean>(() => userContext.features.enableSchema);
this.databases = ko.observableArray<ViewModels.Database>();
this.canSaveQueries = ko.computed<boolean>(() => {
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<ViewModels.TreeNode>(); this.selectedNode = ko.observable<ViewModels.TreeNode>();
this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => {
// Make sure switching tabs restores tabs display // Make sure switching tabs restores tabs display
@ -641,34 +620,14 @@ export default class Explorer {
return null; return null;
} }
if (this.selectedNode().nodeKind === "Database") { 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; 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 { public isSelectedDatabaseShared(): boolean {
const database = this.findSelectedDatabase(); const database = this.findSelectedDatabase();
if (!!database) { if (!!database) {
@ -691,10 +650,11 @@ export default class Explorer {
let loadCollectionPromises: Q.Promise<void>[] = []; let loadCollectionPromises: Q.Promise<void>[] = [];
// If the user has a lot of databases, only load expanded databases. // If the user has a lot of databases, only load expanded databases.
const databases = useDatabases.getState().databases;
const databasesToLoad = const databasesToLoad =
this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand databases.length <= Explorer.MaxNbDatabasesToAutoExpand
? this.databases() ? databases
: this.databases().filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName); : databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName);
const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, {
dataExplorerArea: Constants.Areas.ResourceTree, 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( private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[] updatedDatabaseList: DataModels.Database[]
): { ): {
toAdd: ViewModels.Database[]; toAdd: ViewModels.Database[];
toDelete: ViewModels.Database[]; toDelete: ViewModels.Database[];
} { } {
const databases = useDatabases.getState().databases;
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some( const databaseExists = _.some(
this.databases(), databases,
(existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id (existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id
); );
return !databaseExists; return !databaseExists;
@ -779,7 +718,7 @@ export default class Explorer {
); );
let databasesToDelete: ViewModels.Database[] = []; let databasesToDelete: ViewModels.Database[] = [];
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { ko.utils.arrayForEach(databases, (database: ViewModels.Database) => {
const databasePresentInUpdatedList = _.some( const databasePresentInUpdatedList = _.some(
updatedDatabaseList, updatedDatabaseList,
(db: DataModels.Database) => db.id === database.id() (db: DataModels.Database) => db.id === database.id()
@ -793,24 +732,12 @@ export default class Explorer {
} }
private addDatabasesToList(databases: ViewModels.Database[]): void { private addDatabasesToList(databases: ViewModels.Database[]): void {
this.databases( useDatabases.getState().addDatabases(databases);
this.databases()
.concat(databases)
.sort((database1, database2) => database1.id().localeCompare(database2.id()))
);
} }
private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void { private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void {
const databasesToKeep: ViewModels.Database[] = []; const deleteDatabase = useDatabases.getState().deleteDatabase;
databasesToRemove.forEach((database) => deleteDatabase(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);
} }
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> { public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
@ -1414,34 +1341,6 @@ export default class Explorer {
} }
} }
public async loadDatabaseOffers(): Promise<void> {
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 { public openDeleteCollectionConfirmationPane(): void {
useSidePanel useSidePanel
.getState() .getState()
@ -1466,7 +1365,7 @@ export default class Explorer {
} }
public async openAddCollectionPanel(databaseId?: string): Promise<void> { public async openAddCollectionPanel(databaseId?: string): Promise<void> {
await this.loadDatabaseOffers(); await useDatabases.getState().loadDatabaseOffers();
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />); .openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);

View File

@ -31,6 +31,7 @@ import { getUpsellMessage } from "../../Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { PanelFooterComponent } from "./PanelFooterComponent"; import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen"; import { PanelLoadingScreen } from "./PanelLoadingScreen";
@ -125,6 +126,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
render(): JSX.Element { render(): JSX.Element {
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
return ( return (
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}> <form className="panelFormWrapper" onSubmit={this.submit.bind(this)}>
{this.state.errorMessage && ( {this.state.errorMessage && (
@ -137,7 +140,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!this.state.errorMessage && this.isFreeTierAccount() && ( {!this.state.errorMessage && this.isFreeTierAccount() && (
<PanelInfoErrorComponent <PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, this.props.explorer.isFirstResourceCreated(), true)} message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
messageType="info" messageType="info"
showErrorDetails={false} showErrorDetails={false}
link={Constants.Urls.freeTierInformation} link={Constants.Urls.freeTierInformation}
@ -240,9 +243,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && this.state.isSharedThroughputChecked && ( {!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={ showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
isDatabase={true} isDatabase={true}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)} setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
@ -469,9 +470,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowCollectionThroughputInput() && ( {this.shouldShowCollectionThroughputInput() && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={ showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
isDatabase={false} isDatabase={false}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)} setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
@ -680,7 +679,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private getDatabaseOptions(): IDropdownOption[] { private getDatabaseOptions(): IDropdownOption[] {
return this.props.explorer?.databases()?.map((database) => ({ return useDatabases.getState().databases?.map((database) => ({
key: database.id(), key: database.id(),
text: database.id(), text: database.id(),
})); }));
@ -772,9 +771,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false; return false;
} }
const selectedDatabase = this.props.explorer const selectedDatabase = useDatabases
.databases() .getState()
?.find((database) => database.id() === this.state.selectedDatabaseId); .databases?.find((database) => database.id() === this.state.selectedDatabaseId);
return !!selectedDatabase?.offer(); return !!selectedDatabase?.offer();
} }

View File

@ -16,6 +16,7 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../../Utils/PricingUtils"; import { getUpsellMessage } from "../../../Utils/PricingUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { getTextFieldStyles } from "../PanelStyles"; import { getTextFieldStyles } from "../PanelStyles";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@ -172,7 +173,12 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
<RightPaneForm {...props}> <RightPaneForm {...props}>
{!formErrors && isFreeTierAccount && ( {!formErrors && isFreeTierAccount && (
<PanelInfoErrorComponent <PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, container.isFirstResourceCreated(), true)} message={getUpsellMessage(
userContext.portalEnv,
true,
useDatabases.getState().isFirstResourceCreated(),
true
)}
messageType="info" messageType="info"
showErrorDetails={false} showErrorDetails={false}
link={Constants.Urls.freeTierInformation} link={Constants.Urls.freeTierInformation}
@ -225,7 +231,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
{!isServerlessAccount() && databaseCreateNewShared && ( {!isServerlessAccount() && databaseCreateNewShared && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()} showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={true} isDatabase={true}
isSharded={databaseCreateNewShared} isSharded={databaseCreateNewShared}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)} setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}

View File

@ -1,14 +1,16 @@
import { mount } from "enzyme"; import { mount } from "enzyme";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import { SavedQueries } from "../../../Common/Constants";
import { QueriesClient } from "../../../Common/QueriesClient"; import { QueriesClient } from "../../../Common/QueriesClient";
import { Query } from "../../../Contracts/DataModels"; import { Query } from "../../../Contracts/DataModels";
import { Collection, Database } from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { BrowseQueriesPane } from "./BrowseQueriesPane"; import { BrowseQueriesPane } from "./BrowseQueriesPane";
describe("Browse queries panel", () => { describe("Browse queries panel", () => {
const fakeExplorer = {} as Explorer; const fakeExplorer = {} as Explorer;
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
const fakeClientQuery = {} as QueriesClient; const fakeClientQuery = {} as QueriesClient;
const fakeQueryData = [] as Query[]; const fakeQueryData = [] as Query[];
fakeClientQuery.getQueries = async () => fakeQueryData; fakeClientQuery.getQueries = async () => fakeQueryData;
@ -17,6 +19,16 @@ describe("Browse queries panel", () => {
explorer: fakeExplorer, explorer: fakeExplorer,
closePanel: (): void => undefined, 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", () => { it("Should render Default properly", () => {
const wrapper = mount(<BrowseQueriesPane {...props} />); const wrapper = mount(<BrowseQueriesPane {...props} />);

View File

@ -13,6 +13,7 @@ import {
} from "../../Controls/QueriesGridReactComponent/QueriesGridComponent"; } from "../../Controls/QueriesGridReactComponent/QueriesGridComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab"; import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
import { useDatabases } from "../../useDatabases";
interface BrowseQueriesPaneProps { interface BrowseQueriesPaneProps {
explorer: Explorer; explorer: Explorer;
@ -45,12 +46,13 @@ export const BrowseQueriesPane: FunctionComponent<BrowseQueriesPaneProps> = ({
}); });
closeSidePanel(); closeSidePanel();
}; };
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
const props: QueriesGridComponentProps = { const props: QueriesGridComponentProps = {
queriesClient: explorer.queriesClient, queriesClient: explorer.queriesClient,
onQuerySelect: loadSavedQuery, onQuerySelect: loadSavedQuery,
containerVisible: true, containerVisible: true,
saveQueryEnabled: explorer.canSaveQueries(), saveQueryEnabled: isSaveQueryEnabled(),
}; };
return ( return (

View File

@ -5,7 +5,6 @@ exports[`Browse queries panel Should render Default properly 1`] = `
closePanel={[Function]} closePanel={[Function]}
explorer={ explorer={
Object { Object {
"canSaveQueries": [Function],
"queriesClient": Object { "queriesClient": Object {
"getQueries": [Function], "getQueries": [Function],
}, },

View File

@ -12,6 +12,7 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
import { useDatabases } from "../../useDatabases";
import { getTextFieldStyles } from "../PanelStyles"; import { getTextFieldStyles } from "../PanelStyles";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@ -236,7 +237,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
styles={{ root: { width: 300 }, title: { fontSize: 12 }, dropdownItem: { fontSize: 12 } }} styles={{ root: { width: 300 }, title: { fontSize: 12 }, dropdownItem: { fontSize: 12 } }}
placeholder="Choose existing keyspace id" placeholder="Choose existing keyspace id"
defaultSelectedKey={existingKeyspaceId} defaultSelectedKey={existingKeyspaceId}
options={container?.databases()?.map((keyspace) => ({ options={useDatabases.getState().databases?.map((keyspace) => ({
key: keyspace.id(), key: keyspace.id(),
text: keyspace.id(), text: keyspace.id(),
data: { data: {
@ -253,7 +254,9 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
{!isServerlessAccount() && keyspaceCreateNew && isKeyspaceShared && ( {!isServerlessAccount() && keyspaceCreateNew && isKeyspaceShared && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()} showFreeTierExceedThroughputTooltip={
isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()
}
isDatabase isDatabase
isSharded isSharded
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)} setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
@ -324,7 +327,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
)} )}
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && ( {!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()} showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false} isDatabase={false}
isSharded={false} isSharded={false}
setThroughputValue={(throughput: number) => (tableThroughput = throughput)} setThroughputValue={(throughput: number) => (tableThroughput = throughput)}

View File

@ -11,44 +11,42 @@ import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryCons
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane";
describe("Delete Collection Confirmation Pane", () => { describe("Delete Collection Confirmation Pane", () => {
describe("Explorer.isLastCollection()", () => { describe("useDatabases.isLastCollection()", () => {
let explorer: Explorer; beforeAll(() => useDatabases.getState().clearDatabases());
afterEach(() => useDatabases.getState().clearDatabases());
beforeEach(() => {
explorer = new Explorer();
});
it("should be true if 1 database and 1 collection", () => { it("should be true if 1 database and 1 collection", () => {
const database = {} as Database; const database = { id: ko.observable("testDB") } as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]); database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
explorer.databases = ko.observableArray<Database>([database]); useDatabases.getState().addDatabases([database]);
expect(explorer.isLastCollection()).toBe(true); expect(useDatabases.getState().isLastCollection()).toBe(true);
}); });
it("should be false if if 1 database and 2 collection", () => { it("should be false if if 1 database and 2 collection", () => {
const database = {} as Database; const database = { id: ko.observable("testDB") } as Database;
database.collections = ko.observableArray<Collection>([{} as Collection, {} as Collection]); database.collections = ko.observableArray<Collection>([
explorer.databases = ko.observableArray<Database>([database]); { id: ko.observable("coll1") } as Collection,
expect(explorer.isLastCollection()).toBe(false); { 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", () => { it("should be false if 2 database and 1 collection each", () => {
const database = {} as Database; const database = { id: ko.observable("testDB") } as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]); database.collections = ko.observableArray<Collection>([{ id: ko.observable("coll1") } as Collection]);
const database2 = {} as Database; const database2 = { id: ko.observable("testDB2") } as Database;
database2.collections = ko.observableArray<Collection>([{} as Collection]); database2.collections = ko.observableArray<Collection>([{ id: ko.observable("coll2") } as Collection]);
explorer.databases = ko.observableArray<Database>([database, database2]); useDatabases.getState().addDatabases([database, database2]);
expect(explorer.isLastCollection()).toBe(false); expect(useDatabases.getState().isLastCollection()).toBe(false);
}); });
it("should be false if 0 databases", () => { it("should be false if 0 databases", () => {
const database = {} as Database; expect(useDatabases.getState().isLastCollection()).toBe(false);
explorer.databases = ko.observableArray<Database>();
database.collections = ko.observableArray<Collection>();
expect(explorer.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", () => { it("should return true if last collection and database does not have shared throughput else false", () => {
const fakeExplorer = new Explorer(); const fakeExplorer = new Explorer();
fakeExplorer.refreshAllDatabases = () => undefined; fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false; fakeExplorer.isSelectedDatabaseShared = () => false;
const props = { const props = {
@ -65,15 +62,15 @@ describe("Delete Collection Confirmation Pane", () => {
collectionName: "container", collectionName: "container",
}; };
const wrapper = shallow(<DeleteCollectionConfirmationPane {...props} />); const wrapper = shallow(<DeleteCollectionConfirmationPane {...props} />);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
props.explorer.isLastCollection = () => true;
props.explorer.isSelectedDatabaseShared = () => true;
wrapper.setProps(props);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
props.explorer.isLastCollection = () => false; const database = { id: ko.observable("testDB") } as Database;
props.explorer.isSelectedDatabaseShared = () => false; database.collections = ko.observableArray<Collection>([{ 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); wrapper.setProps(props);
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
}); });
@ -94,8 +91,10 @@ describe("Delete Collection Confirmation Pane", () => {
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId); fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
fakeExplorer.selectedNode = ko.observable<TreeNode>(); fakeExplorer.selectedNode = ko.observable<TreeNode>();
fakeExplorer.refreshAllDatabases = () => undefined; fakeExplorer.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false; 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]);
beforeAll(() => { beforeAll(() => {
updateUserContext({ updateUserContext({

View File

@ -13,7 +13,9 @@ import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils"; import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface DeleteCollectionConfirmationPaneProps { export interface DeleteCollectionConfirmationPaneProps {
explorer: Explorer; explorer: Explorer;
} }
@ -22,13 +24,14 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
explorer, explorer,
}: DeleteCollectionConfirmationPaneProps) => { }: DeleteCollectionConfirmationPaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const isLastCollection = useDatabases((state) => state.isLastCollection);
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>(""); const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
const [inputCollectionName, setInputCollectionName] = useState<string>(""); const [inputCollectionName, setInputCollectionName] = useState<string>("");
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [isExecuting, setIsExecuting] = useState(false); const [isExecuting, setIsExecuting] = useState(false);
const shouldRecordFeedback = (): boolean => { const shouldRecordFeedback = (): boolean => {
return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared(); return isLastCollection() && !explorer.isSelectedDatabaseShared();
}; };
const collectionName = getCollectionName().toLocaleLowerCase(); const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = "Delete " + collectionName; const paneTitle = "Delete " + collectionName;

View File

@ -7,7 +7,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
explorer={ explorer={
Object { Object {
"findSelectedCollection": [Function], "findSelectedCollection": [Function],
"isLastCollection": [Function],
"isSelectedDatabaseShared": [Function], "isSelectedDatabaseShared": [Function],
"refreshAllDatabases": [Function], "refreshAllDatabases": [Function],
"selectedCollectionId": [Function], "selectedCollectionId": [Function],

View File

@ -11,19 +11,20 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { TabsManager } from "../Tabs/TabsManager";
import { useDatabases } from "../useDatabases";
import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel";
describe("Delete Database Confirmation Pane", () => { describe("Delete Database Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => { describe("shouldRecordFeedback()", () => {
it("should return true if last non empty database or is last database that has shared throughput, else false", () => { 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.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false; fakeExplorer.isSelectedDatabaseShared = () => false;
const database = {} as Database; const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]); database.collections = ko.observableArray<Collection>([{ id: ko.observable("testCollection") } as Collection]);
database.id = ko.observable<string>("testDatabse"); database.id = ko.observable<string>("testDatabase");
const props = { const props = {
explorer: fakeExplorer, explorer: fakeExplorer,
@ -33,29 +34,26 @@ describe("Delete Database Confirmation Pane", () => {
}; };
const wrapper = shallow(<DeleteDatabaseConfirmationPanel {...props} />); const wrapper = shallow(<DeleteDatabaseConfirmationPanel {...props} />);
props.explorer.isLastNonEmptyDatabase = () => true; expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false);
useDatabases.getState().addDatabases([database]);
wrapper.setProps(props); wrapper.setProps(props);
expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true); expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true);
useDatabases.getState().clearDatabases();
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);
}); });
}); });
describe("submit()", () => { describe("submit()", () => {
const selectedDatabaseId = "testDatabse"; const selectedDatabaseId = "testDatabse";
const fakeExplorer = new Explorer(); 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.refreshAllDatabases = () => undefined;
fakeExplorer.isLastCollection = () => true;
fakeExplorer.isSelectedDatabaseShared = () => false; fakeExplorer.isSelectedDatabaseShared = () => false;
fakeExplorer.tabsManager = new TabsManager();
fakeExplorer.selectedNode = ko.observable();
let wrapper: ReactWrapper; let wrapper: ReactWrapper;
beforeAll(() => { beforeAll(() => {
@ -71,13 +69,10 @@ describe("Delete Database Confirmation Pane", () => {
}); });
(deleteDatabase as jest.Mock).mockResolvedValue(undefined); (deleteDatabase as jest.Mock).mockResolvedValue(undefined);
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined); (TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);
useDatabases.getState().addDatabases([database]);
}); });
beforeEach(() => { beforeEach(() => {
const database = {} as Database;
database.collections = ko.observableArray<Collection>([{} as Collection]);
database.id = ko.observable<string>(selectedDatabaseId);
const props = { const props = {
explorer: fakeExplorer, explorer: fakeExplorer,
closePanel: (): void => undefined, closePanel: (): void => undefined,
@ -86,10 +81,10 @@ describe("Delete Database Confirmation Pane", () => {
}; };
wrapper = mount(<DeleteDatabaseConfirmationPanel {...props} />); wrapper = mount(<DeleteDatabaseConfirmationPanel {...props} />);
props.explorer.isLastNonEmptyDatabase = () => true;
wrapper.setProps(props);
}); });
afterAll(() => useDatabases.getState().clearDatabases());
it("Should call delete database", () => { it("Should call delete database", () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#confirmDatabaseId")).toBe(true); expect(wrapper.exists("#confirmDatabaseId")).toBe(true);

View File

@ -13,6 +13,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useDatabases } from "../useDatabases";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
@ -26,6 +27,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
selectedDatabase, selectedDatabase,
}: DeleteDatabaseConfirmationPanelProps): JSX.Element => { }: DeleteDatabaseConfirmationPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase);
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
@ -70,7 +72,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
startKey startKey
); );
if (shouldRecordFeedback()) { if (isLastNonEmptyDatabase()) {
const deleteFeedback = new DeleteFeedback( const deleteFeedback = new DeleteFeedback(
userContext?.databaseAccount.id, userContext?.databaseAccount.id,
userContext?.databaseAccount.name, userContext?.databaseAccount.name,
@ -100,10 +102,6 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
} }
}; };
const shouldRecordFeedback = (): boolean => {
return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared());
};
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError, formError,
isExecuting: isLoading, isExecuting: isLoading,
@ -134,7 +132,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
}} }}
/> />
</div> </div>
{shouldRecordFeedback() && ( {isLastNonEmptyDatabase() && (
<div className="deleteDatabaseFeedback"> <div className="deleteDatabaseFeedback">
<Text variant="small" block> <Text variant="small" block>
Help us improve Azure Cosmos DB! Help us improve Azure Cosmos DB!

View File

@ -19,8 +19,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"databases": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],

View File

@ -1,32 +1,38 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import { SavedQueries } from "../../../Common/Constants";
import { Collection, Database } from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { SaveQueryPane } from "./SaveQueryPane"; import { SaveQueryPane } from "./SaveQueryPane";
describe("Save Query Pane", () => { describe("Save Query Pane", () => {
const fakeExplorer = {} as Explorer; const fakeExplorer = {} as Explorer;
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
const props = { const props = {
explorer: fakeExplorer, explorer: fakeExplorer,
closePanel: (): void => undefined, closePanel: (): void => undefined,
}; };
const wrapper = shallow(<SaveQueryPane {...props} />);
it("should return true if can save Queries else false", () => {
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => true);
wrapper.setProps(props);
expect(wrapper.exists("#saveQueryInput")).toBe(true);
fakeExplorer.canSaveQueries = ko.computed<boolean>(() => false);
wrapper.setProps(props);
expect(wrapper.exists("#saveQueryInput")).toBe(false);
});
it("should render Default properly", () => { it("should render Default properly", () => {
const wrapper = shallow(<SaveQueryPane {...props} />); const wrapper = shallow(<SaveQueryPane {...props} />);
expect(wrapper.exists("#saveQueryInput")).toBe(false);
expect(wrapper).toMatchSnapshot(); 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(<SaveQueryPane {...props} />);
expect(wrapper.exists("#saveQueryInput")).toBe(true);
});
}); });

View File

@ -10,6 +10,7 @@ import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetr
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab"; import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
import { useDatabases } from "../../useDatabases";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
interface SaveQueryPaneProps { interface SaveQueryPaneProps {
@ -24,11 +25,11 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ 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 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 title = "Save Query";
const { canSaveQueries } = explorer; const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
setFormError(""); setFormError("");
if (!canSaveQueries()) { if (!isSaveQueryEnabled()) {
setFormError("Cannot save query"); setFormError("Cannot save query");
logConsoleError("Failed to save query: account not setup to save queries"); logConsoleError("Failed to save query: account not setup to save queries");
} }
@ -129,16 +130,16 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError: formError, formError: formError,
isExecuting: isLoading, isExecuting: isLoading,
submitButtonText: canSaveQueries() ? "Save" : "Complete setup", submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup",
onSubmit: () => { onSubmit: () => {
canSaveQueries() ? submit() : setupQueries(); isSaveQueryEnabled() ? submit() : setupQueries();
}, },
}; };
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
{!canSaveQueries() ? ( {!isSaveQueryEnabled() ? (
<Text variant="small">{setupSaveQueriesText}</Text> <Text variant="small">{setupSaveQueriesText}</Text>
) : ( ) : (
<TextField <TextField

View File

@ -9,8 +9,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"databases": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],

View File

@ -4,50 +4,10 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
<DeleteDatabaseConfirmationPanel <DeleteDatabaseConfirmationPanel
closePanel={[Function]} closePanel={[Function]}
explorer={ explorer={
Explorer { Object {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"canSaveQueries": [Function],
"databases": [Function],
"isAccountReady": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isLastCollection": [Function],
"isLastNonEmptyDatabase": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function],
"isSelectedDatabaseShared": [Function], "isSelectedDatabaseShared": [Function],
"isShellEnabled": [Function],
"isSynapseLinkUpdating": [Function],
"isTabsContentExpanded": [Function],
"memoryUsageInfo": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshAllDatabases": [Function], "refreshAllDatabases": [Function],
"refreshNotebookList": [Function],
"resourceTokenCollection": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
"container": [Circular],
"parameters": [Function],
},
"selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"sparkClusterConnectionInfo": [Function],
"tabsManager": TabsManager { "tabsManager": TabsManager {
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
@ -749,7 +709,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small" variant="small"
> >
<span <span
className="css-77" className="css-69"
> >
Help us improve Azure Cosmos DB! Help us improve Azure Cosmos DB!
</span> </span>
@ -759,7 +719,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
variant="small" variant="small"
> >
<span <span
className="css-77" className="css-69"
> >
What is the reason why you are deleting this database? What is the reason why you are deleting this database?
</span> </span>
@ -1067,11 +1027,11 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
> >
<div <div
className="ms-TextField-fieldGroup fieldGroup-78" className="ms-TextField-fieldGroup fieldGroup-70"
> >
<textarea <textarea
aria-invalid={false} aria-invalid={false}
className="ms-TextField-field field-79" className="ms-TextField-field field-71"
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@ -2797,7 +2757,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
> >
<button <button
aria-label="OK" aria-label="OK"
className="ms-Button ms-Button--primary root-69" className="ms-Button ms-Button--primary root-73"
data-is-focusable={true} data-is-focusable={true}
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
@ -2809,16 +2769,16 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
type="submit" type="submit"
> >
<span <span
className="ms-Button-flexContainer flexContainer-70" className="ms-Button-flexContainer flexContainer-74"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<span <span
className="ms-Button-textContainer textContainer-71" className="ms-Button-textContainer textContainer-75"
> >
<span <span
className="ms-Button-label label-73" className="ms-Button-label label-77"
id="id__3" id="id__6"
key="id__3" key="id__6"
> >
OK OK
</span> </span>

View File

@ -22,6 +22,7 @@ import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLaunc
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil"; import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { useDatabases } from "../useDatabases";
export interface SplashScreenItem { export interface SplashScreenItem {
iconSrc: string; iconSrc: string;
@ -308,8 +309,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: collectionId, title: collectionId,
description: "Data", description: "Data",
onClick: () => { onClick: () => {
const collection = this.container.findCollection(databaseId, collectionId); const collection = useDatabases.getState().findCollection(databaseId, collectionId);
collection && collection.openTab(); collection?.openTab();
}, },
}; };
} }

View File

@ -33,6 +33,7 @@ import MongoShellTab from "../Tabs/MongoShellTab";
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab"; import QueryTablesTab from "../Tabs/QueryTablesTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases";
import ConflictId from "./ConflictId"; import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId"; import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure"; import StoredProcedure from "./StoredProcedure";
@ -1153,7 +1154,7 @@ export default class Collection implements ViewModels.Collection {
} }
public getDatabase(): ViewModels.Database { public getDatabase(): ViewModels.Database {
return this.container.findDatabaseWithId(this.databaseId); return useDatabases.getState().findDatabaseWithId(this.databaseId);
} }
public async loadOffer(): Promise<void> { public async loadOffer(): Promise<void> {

View File

@ -9,6 +9,7 @@ import Explorer from "../Explorer";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import DocumentId from "./DocumentId"; import DocumentId from "./DocumentId";
export default class ResourceTokenCollection implements ViewModels.CollectionBase { export default class ResourceTokenCollection implements ViewModels.CollectionBase {
@ -151,6 +152,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
} }
public getDatabase(): ViewModels.Database { public getDatabase(): ViewModels.Database {
return this.container.findDatabaseWithId(this.databaseId); return useDatabases.getState().findDatabaseWithId(this.databaseId);
} }
} }

View File

@ -33,6 +33,7 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil"; import { NotebookUtil } from "../Notebook/NotebookUtil";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import StoredProcedure from "./StoredProcedure"; import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger"; import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction"; import UserDefinedFunction from "./UserDefinedFunction";
@ -66,15 +67,18 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.koSubsCollectionIdMap = new ArrayHashMap(); this.koSubsCollectionIdMap = new ArrayHashMap();
this.databaseCollectionIdMap = new ArrayHashMap(); this.databaseCollectionIdMap = new ArrayHashMap();
this.container.databases.subscribe((databases: ViewModels.Database[]) => { useDatabases.subscribe(
// Clean up old databases (databases: ViewModels.Database[]) => {
this.cleanupDatabasesKoSubs(); // Clean up old databases
this.cleanupDatabasesKoSubs();
databases.forEach((database: ViewModels.Database) => this.watchDatabase(database)); databases.forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.triggerRender(); this.triggerRender();
}); },
(state) => state.databases
);
this.container.databases().forEach((database: ViewModels.Database) => this.watchDatabase(database)); useDatabases.getState().databases.forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.triggerRender(); this.triggerRender();
} }
@ -192,7 +196,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
} }
private buildDataTree(): TreeNode { private buildDataTree(): TreeNode {
const databaseTreeNodes: TreeNode[] = this.container.databases().map((database: ViewModels.Database) => { const databaseTreeNodes: TreeNode[] = useDatabases.getState().databases.map((database: ViewModels.Database) => {
const databaseNode: TreeNode = { const databaseNode: TreeNode = {
label: database.id(), label: database.id(),
iconSrc: CosmosDBIcon, iconSrc: CosmosDBIcon,
@ -882,7 +886,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.addKoSubToCollectionId( this.addKoSubToCollectionId(
databaseId, databaseId,
collection.id(), collection.id(),
collection.storedProcedures.subscribe(() => { collection.storedProcedures?.subscribe(() => {
this.triggerRender(); this.triggerRender();
}) })
); );
@ -890,7 +894,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.addKoSubToCollectionId( this.addKoSubToCollectionId(
databaseId, databaseId,
collection.id(), collection.id(),
collection.isCollectionExpanded.subscribe(() => { collection.isCollectionExpanded?.subscribe(() => {
this.triggerRender(); this.triggerRender();
}) })
); );
@ -898,7 +902,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.addKoSubToCollectionId( this.addKoSubToCollectionId(
databaseId, databaseId,
collection.id(), collection.id(),
collection.isStoredProceduresExpanded.subscribe(() => { collection.isStoredProceduresExpanded?.subscribe(() => {
this.triggerRender(); this.triggerRender();
}) })
); );

View File

@ -0,0 +1,113 @@
import _ from "underscore";
import create, { UseStore } from "zustand";
import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels";
interface DatabasesState {
databases: ViewModels.Database[];
updateDatabase: (database: ViewModels.Database) => void;
addDatabases: (databases: ViewModels.Database[]) => void;
deleteDatabase: (database: ViewModels.Database) => void;
clearDatabases: () => void;
isSaveQueryEnabled: () => boolean;
findDatabaseWithId: (databaseId: string) => ViewModels.Database;
isLastNonEmptyDatabase: () => boolean;
findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection;
isLastCollection: () => boolean;
loadDatabaseOffers: () => Promise<void>;
isFirstResourceCreated: () => boolean;
}
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
databases: [],
updateDatabase: (updatedDatabase: ViewModels.Database) =>
set((state) => {
const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
if (database.id() === updatedDatabase.id()) {
return updatedDatabase;
}
return database;
});
return { databases: updatedDatabases };
}),
addDatabases: (databases: ViewModels.Database[]) =>
set((state) => ({
databases: [...state.databases, ...databases].sort((db1, db2) => db1.id().localeCompare(db2.id())),
})),
deleteDatabase: (database: ViewModels.Database) =>
set((state) => ({ databases: state.databases.filter((db) => database.id() !== db.id()) })),
clearDatabases: () => set(() => ({ databases: [] })),
isSaveQueryEnabled: () => {
const savedQueriesDatabase: ViewModels.Database = _.find(
get().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;
},
findDatabaseWithId: (databaseId: string) => get().databases.find((db) => databaseId === db.id()),
isLastNonEmptyDatabase: () => {
const databases = get().databases;
return databases.length === 1 && (databases[0].collections()?.length > 0 || !!databases[0].offer());
},
findCollection: (databaseId: string, collectionId: string) => {
const database = get().findDatabaseWithId(databaseId);
return database?.collections()?.find((collection) => collection.id() === collectionId);
},
isLastCollection: () => {
const databases = get().databases;
if (databases.length === 0) {
return false;
}
let collectionCount = 0;
for (let i = 0; i < databases.length; i++) {
const database = databases[i];
collectionCount += database.collections().length;
if (collectionCount > 1) {
return false;
}
}
return true;
},
loadDatabaseOffers: async () => {
await Promise.all(
get().databases?.map(async (database: ViewModels.Database) => {
await database.loadOffer();
})
);
},
isFirstResourceCreated: () => {
const databases = get().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;
});
},
}));

View File

@ -5,6 +5,7 @@ import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import ScriptTabBase from "../Explorer/Tabs/ScriptTabBase"; import ScriptTabBase from "../Explorer/Tabs/ScriptTabBase";
import TabsBase from "../Explorer/Tabs/TabsBase"; import TabsBase from "../Explorer/Tabs/TabsBase";
import { useDatabases } from "../Explorer/useDatabases";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export class TabRouteHandler { export class TabRouteHandler {
@ -248,12 +249,11 @@ export class TabRouteHandler {
private _openDatabaseSettingsTabForResource(databaseId: string): void { private _openDatabaseSettingsTabForResource(databaseId: string): void {
this._executeActionHelper(() => { this._executeActionHelper(() => {
const explorer = window.dataExplorer;
const database: ViewModels.Database = _.find( const database: ViewModels.Database = _.find(
explorer.databases(), useDatabases.getState().databases,
(database: ViewModels.Database) => database.id() === databaseId (database: ViewModels.Database) => database.id() === databaseId
); );
database && database.onSettingsClick(); database?.onSettingsClick();
}); });
} }
@ -391,7 +391,7 @@ export class TabRouteHandler {
private _findMatchingCollectionForResource(databaseId: string, collectionId: string): ViewModels.Collection { private _findMatchingCollectionForResource(databaseId: string, collectionId: string): ViewModels.Collection {
const explorer = window.dataExplorer; const explorer = window.dataExplorer;
const matchedDatabase: ViewModels.Database = explorer.findDatabaseWithId(databaseId); const matchedDatabase: ViewModels.Database = useDatabases.getState().findDatabaseWithId(databaseId);
const matchedCollection: ViewModels.Collection = const matchedCollection: ViewModels.Collection =
matchedDatabase && matchedDatabase.findCollectionWithId(collectionId); matchedDatabase && matchedDatabase.findCollectionWithId(collectionId);

View File

@ -1,64 +0,0 @@
import * as ko from "knockout";
import { Collection, Database } from "../Contracts/ViewModels";
import { getMaxThroughput } from "./AddCollectionUtility";
import Explorer from "../Explorer/Explorer";
describe("getMaxThroughput", () => {
it("default unlimited throughput setting", () => {
const defaults = {
storage: "100",
throughput: {
fixed: 400,
unlimited: 400,
unlimitedmax: 1000000,
unlimitedmin: 400,
shared: 400,
},
};
expect(getMaxThroughput(defaults, {} as Explorer)).toEqual(defaults.throughput.unlimited);
});
describe("no unlimited throughput setting", () => {
const defaults = {
storage: "100",
throughput: {
fixed: 400,
unlimited: {
collectionThreshold: 3,
lessThanOrEqualToThreshold: 400,
greatThanThreshold: 500,
},
unlimitedmax: 1000000,
unlimitedmin: 400,
shared: 400,
},
};
const mockCollection1 = { id: ko.observable("collection1") } as Collection;
const mockCollection2 = { id: ko.observable("collection2") } as Collection;
const mockCollection3 = { id: ko.observable("collection3") } as Collection;
const mockCollection4 = { id: ko.observable("collection4") } as Collection;
const mockDatabase = {} as Database;
const mockContainer = {
databases: ko.observableArray([mockDatabase]),
} as Explorer;
it("less than or equal to collection threshold", () => {
mockDatabase.collections = ko.observableArray([mockCollection1, mockCollection2]);
expect(getMaxThroughput(defaults, mockContainer)).toEqual(
defaults.throughput.unlimited.lessThanOrEqualToThreshold
);
});
it("exceeds collection threshold", () => {
mockDatabase.collections = ko.observableArray([
mockCollection1,
mockCollection2,
mockCollection3,
mockCollection4,
]);
expect(getMaxThroughput(defaults, mockContainer)).toEqual(defaults.throughput.unlimited.greatThanThreshold);
});
});
});

View File

@ -1,23 +0,0 @@
import { any } from "underscore";
import Explorer from "../Explorer/Explorer";
import { CollectionCreationDefaults } from "../UserContext";
export const getMaxThroughput = (defaults: CollectionCreationDefaults, container: Explorer): number => {
const throughput = defaults.throughput.unlimited;
if (typeof throughput === "number") {
return throughput;
} else {
return _exceedsThreshold(throughput.collectionThreshold, container)
? throughput.greatThanThreshold
: throughput.lessThanOrEqualToThreshold;
}
};
const _exceedsThreshold = (unlimitedThreshold: number, container: Explorer): boolean => {
const databases = (container && container.databases && container.databases()) || [];
return any(
databases,
(database) =>
database && database.collections && database.collections() && database.collections().length > unlimitedThreshold
);
};

View File

@ -10,6 +10,7 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels";
import Explorer, { ExplorerParams } from "../Explorer/Explorer"; import Explorer, { ExplorerParams } from "../Explorer/Explorer";
import { handleOpenAction } from "../Explorer/OpenActions/OpenActions"; import { handleOpenAction } from "../Explorer/OpenActions/OpenActions";
import { useDatabases } from "../Explorer/useDatabases";
import { import {
AAD, AAD,
ConnectionString, ConnectionString,
@ -253,7 +254,7 @@ async function configurePortal(explorerParams: ExplorerParams): Promise<Explorer
const explorer = new Explorer(explorerParams); const explorer = new Explorer(explorerParams);
resolve(explorer); resolve(explorer);
if (openAction) { if (openAction) {
handleOpenAction(openAction, explorer.databases(), explorer); handleOpenAction(openAction, useDatabases.getState().databases, explorer);
} }
} else if (shouldForwardMessage(message, event.origin)) { } else if (shouldForwardMessage(message, event.origin)) {
sendMessage(message); sendMessage(message);