victor-meng 46ca952955
Add condition for showing quick start carousel (#1278)
* Add condition for showing quick start carousel

* Show coach mark when carousel is closed

* Add condition for showing quick start carousel and other UI changes

* Fix compile error

* Fix issue with coach mark

* Fix test

* Add new sample data, fix link url, fix e2e tests

* Fix e2e tests
2022-05-23 20:52:21 -07:00

1231 lines
42 KiB
TypeScript

import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
import { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { UploadDetailsRecord } from "../../Contracts/ViewModels";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentsTab from "../Tabs/DocumentsTab";
import GraphTab from "../Tabs/GraphTab";
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab";
import { NewMongoShellTab } from "../Tabs/MongoShellTab/MongoShellTab";
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
export default class Collection implements ViewModels.Collection {
public nodeKind: string;
public container: Explorer;
public self: string;
public rid: string;
public databaseId: string;
public partitionKey: DataModels.PartitionKey;
public partitionKeyPropertyHeaders: string[];
public partitionKeyProperties: string[];
public id: ko.Observable<string>;
public defaultTtl: ko.Observable<number>;
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
public usageSizeInKB: ko.Observable<number>;
public offer: ko.Observable<DataModels.Offer>;
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
public partitions: ko.Computed<number>;
public throughput: ko.Computed<number>;
public rawDataModel: DataModels.Collection;
public analyticalStorageTtl: ko.Observable<number>;
public schema: DataModels.ISchema;
public requestSchema: () => void;
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
// TODO move this to API customization class
public cassandraKeys: CassandraTableKeys;
public cassandraSchema: CassandraTableKey[];
public documentIds: ko.ObservableArray<DocumentId>;
public children: ko.ObservableArray<ViewModels.TreeNode>;
public storedProcedures: ko.Computed<StoredProcedure[]>;
public userDefinedFunctions: ko.Computed<UserDefinedFunction[]>;
public triggers: ko.Computed<Trigger[]>;
public showStoredProcedures: ko.Observable<boolean>;
public showTriggers: ko.Observable<boolean>;
public showUserDefinedFunctions: ko.Observable<boolean>;
public showConflicts: ko.Observable<boolean>;
public selectedDocumentContent: ViewModels.Editable<any>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public focusedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public isCollectionExpanded: ko.Observable<boolean>;
public isStoredProceduresExpanded: ko.Observable<boolean>;
public isUserDefinedFunctionsExpanded: ko.Observable<boolean>;
public isTriggersExpanded: ko.Observable<boolean>;
public documentsFocused: ko.Observable<boolean>;
public settingsFocused: ko.Observable<boolean>;
public storedProceduresFocused: ko.Observable<boolean>;
public userDefinedFunctionsFocused: ko.Observable<boolean>;
public triggersFocused: ko.Observable<boolean>;
public isSampleCollection: boolean;
private isOfferRead: boolean;
constructor(container: Explorer, databaseId: string, data: DataModels.Collection) {
this.nodeKind = "Collection";
this.container = container;
this.self = data._self;
this.rid = data._rid;
this.databaseId = databaseId;
this.rawDataModel = data;
this.partitionKey = data.partitionKey;
this.id = ko.observable(data.id);
this.defaultTtl = ko.observable(data.defaultTtl);
this.indexingPolicy = ko.observable(data.indexingPolicy);
this.usageSizeInKB = ko.observable();
this.offer = ko.observable();
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
this.schema = data.schema;
this.requestSchema = data.requestSchema;
this.geospatialConfig = ko.observable(data.geospatialConfig);
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
// TODO fix this to only replace non-excaped single quotes
let partitionKeyProperty = partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, "");
if (userContext.apiType === "Mongo" && partitionKeyProperty) {
if (~partitionKeyProperty.indexOf(`"`)) {
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
}
// TODO #10738269 : Add this logic in a derived class for Mongo
if (partitionKeyProperty.indexOf("$v") > -1) {
// From $v.shard.$v.key.$v > shard.key
partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
this.partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty;
}
}
return partitionKeyProperty;
});
this.documentIds = ko.observableArray<DocumentId>([]);
this.isCollectionExpanded = ko.observable<boolean>(false);
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
this.focusedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
this.documentsFocused = ko.observable<boolean>();
this.documentsFocused.subscribe((focus) => {
console.log("Focus set on Documents: " + focus);
this.focusedSubnodeKind(ViewModels.CollectionTabKind.Documents);
});
this.settingsFocused = ko.observable<boolean>(false);
this.settingsFocused.subscribe((focus) => {
this.focusedSubnodeKind(ViewModels.CollectionTabKind.Settings);
});
this.storedProceduresFocused = ko.observable<boolean>(false);
this.storedProceduresFocused.subscribe((focus) => {
this.focusedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
});
this.userDefinedFunctionsFocused = ko.observable<boolean>(false);
this.userDefinedFunctionsFocused.subscribe((focus) => {
this.focusedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
});
this.triggersFocused = ko.observable<boolean>(false);
this.triggersFocused.subscribe((focus) => {
this.focusedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
});
this.children = ko.observableArray<ViewModels.TreeNode>([]);
this.children.subscribe(() => {
// update the database in zustand store
const database = this.getDatabase();
database.collections(
database.collections()?.map((collection) => {
if (collection.id() === this.id()) {
return this;
}
return collection;
})
);
useDatabases.getState().updateDatabase(database);
});
this.storedProcedures = ko.computed(() => {
return this.children()
.filter((node) => node.nodeKind === "StoredProcedure")
.map((node) => <StoredProcedure>node);
});
this.userDefinedFunctions = ko.computed(() => {
return this.children()
.filter((node) => node.nodeKind === "UserDefinedFunction")
.map((node) => <UserDefinedFunction>node);
});
this.triggers = ko.computed(() => {
return this.children()
.filter((node) => node.nodeKind === "Trigger")
.map((node) => <Trigger>node);
});
const showScriptsMenus: boolean = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
this.showTriggers = ko.observable<boolean>(showScriptsMenus);
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);
this.showConflicts = ko.observable<boolean>(
userContext?.databaseAccount?.properties.enableMultipleWriteLocations && data && !!data.conflictResolutionPolicy
);
this.isStoredProceduresExpanded = ko.observable<boolean>(false);
this.isUserDefinedFunctionsExpanded = ko.observable<boolean>(false);
this.isTriggersExpanded = ko.observable<boolean>(false);
this.isSampleCollection = false;
this.isOfferRead = false;
}
public expandCollapseCollection() {
useSelectedNode.getState().setSelectedNode(this);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Collection node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
if (this.isCollectionExpanded()) {
this.collapseCollection();
} else {
this.expandCollection();
}
useCommandBar.getState().setContextButtons([]);
useTabs
.getState()
.refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
);
}
public collapseCollection() {
if (!this.isCollectionExpanded()) {
return;
}
this.isCollectionExpanded(false);
TelemetryProcessor.trace(Action.CollapseTreeNode, ActionModifiers.Mark, {
description: "Collection node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
}
public expandCollection(): void {
if (this.isCollectionExpanded()) {
return;
}
this.isCollectionExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Collection node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
}
public onDocumentDBDocumentsClick() {
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Documents node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
const documentsTabs: DocumentsTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.Documents,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as DocumentsTab[];
let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0];
if (documentsTab) {
useTabs.getState().activateTab(documentsTab);
} else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.rawDataModel.id + " - Items",
});
this.documentIds([]);
documentsTab = new DocumentsTab({
partitionKey: this.partitionKey,
documentIds: ko.observableArray<DocumentId>([]),
tabKind: ViewModels.CollectionTabKind.Documents,
title: this.rawDataModel.id + " - Items",
collection: this,
node: this,
tabPath: `${this.databaseId}>${this.id()}>Documents`,
onLoadStartKey: startKey,
});
useTabs.getState().activateNewTab(documentsTab);
}
}
public onConflictsClick() {
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Conflicts node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
const conflictsTabs: ConflictsTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.Conflicts,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as ConflictsTab[];
let conflictsTab: ConflictsTab = conflictsTabs && conflictsTabs[0];
if (conflictsTab) {
useTabs.getState().activateTab(conflictsTab);
} else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Conflicts",
});
this.documentIds([]);
const conflictsTab: ConflictsTab = new ConflictsTab({
partitionKey: this.partitionKey,
conflictIds: ko.observableArray<ConflictId>([]),
tabKind: ViewModels.CollectionTabKind.Conflicts,
title: "Conflicts",
collection: this,
node: this,
tabPath: `${this.databaseId}>${this.id()}>Conflicts`,
onLoadStartKey: startKey,
});
useTabs.getState().activateNewTab(conflictsTab);
}
}
public onTableEntitiesClick() {
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.QueryTables);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Entities node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
if (userContext.apiType === "Cassandra" && !this.cassandraKeys) {
(<CassandraAPIDataClient>this.container.tableDataClient).getTableKeys(this).then((keys: CassandraTableKeys) => {
this.cassandraKeys = keys;
});
}
const queryTablesTabs: QueryTablesTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.QueryTables,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as QueryTablesTab[];
let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0];
if (queryTablesTab) {
useTabs.getState().activateTab(queryTablesTab);
} else {
this.documentIds([]);
let title = `Entities`;
if (userContext.apiType === "Cassandra") {
title = `Rows`;
}
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: title,
});
queryTablesTab = new QueryTablesTab({
tabKind: ViewModels.CollectionTabKind.QueryTables,
title: title,
tabPath: "",
collection: this,
node: this,
onLoadStartKey: startKey,
});
useTabs.getState().activateNewTab(queryTablesTab);
}
}
public onGraphDocumentsClick() {
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Documents node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
const graphTabs: GraphTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.Graph,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as GraphTab[];
let graphTab: GraphTab = graphTabs && graphTabs[0];
if (graphTab) {
useTabs.getState().activateTab(graphTab);
} else {
this.documentIds([]);
const title = "Graph";
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: title,
});
graphTab = new GraphTab({
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.Graph,
node: this,
title: title,
tabPath: "",
collection: this,
masterKey: userContext.masterKey || "",
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
collectionId: this.id(),
databaseId: this.databaseId,
isTabsContentExpanded: this.container.isTabsContentExpanded,
onLoadStartKey: startKey,
});
useTabs.getState().activateNewTab(graphTab);
}
}
public onMongoDBDocumentsClick = () => {
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Documents node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
const mongoDocumentsTabs: MongoDocumentsTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.Documents,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as MongoDocumentsTab[];
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0];
if (mongoDocumentsTab) {
useTabs.getState().activateTab(mongoDocumentsTab);
} else {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Documents",
});
this.documentIds([]);
mongoDocumentsTab = new MongoDocumentsTab({
partitionKey: this.partitionKey,
documentIds: this.documentIds,
tabKind: ViewModels.CollectionTabKind.Documents,
title: "Documents",
tabPath: "",
collection: this,
node: this,
onLoadStartKey: startKey,
});
useTabs.getState().activateNewTab(mongoDocumentsTab);
}
};
public onSchemaAnalyzerClick = async () => {
if (useNotebook.getState().isPhoenixFeatures) {
await this.container.allocateContainer();
}
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Schema node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
for (const tab of useTabs.getState().openedTabs) {
if (
tab instanceof SchemaAnalyzerTab &&
tab.collection?.databaseId === this.databaseId &&
tab.collection?.id() === this.id()
) {
return useTabs.getState().activateTab(tab);
}
}
const startKey = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Schema",
});
this.documentIds([]);
useTabs.getState().activateNewTab(
new SchemaAnalyzerTab({
account: userContext.databaseAccount,
masterKey: userContext.masterKey || "",
container: this.container,
tabKind: ViewModels.CollectionTabKind.SchemaAnalyzer,
title: "Schema",
tabPath: "",
collection: this,
node: this,
onLoadStartKey: startKey,
})
);
};
public onSettingsClick = async (): Promise<void> => {
useSelectedNode.getState().setSelectedNode(this);
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Settings node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
const matchingTabs = useTabs.getState().getTabs(ViewModels.CollectionTabKind.CollectionSettingsV2, (tab) => {
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
});
const traceStartData = {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: tabTitle,
};
const settingsTabOptions: ViewModels.TabOptions = {
tabKind: undefined,
title: !this.offer() ? "Settings" : "Scale & Settings",
tabPath: "",
collection: this,
node: this,
};
let settingsTabV2 = matchingTabs && (matchingTabs[0] as CollectionSettingsTabV2);
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions);
};
private launchSettingsTabV2 = (
settingsTabV2: CollectionSettingsTabV2,
traceStartData: any,
settingsTabOptions: ViewModels.TabOptions
): void => {
if (!settingsTabV2) {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, traceStartData);
settingsTabOptions.onLoadStartKey = startKey;
settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2;
settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions);
useTabs.getState().activateNewTab(settingsTabV2);
} else {
useTabs.getState().activateTab(settingsTabV2);
}
};
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source;
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
const title = "Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: title,
});
useTabs.getState().activateNewTab(
new NewQueryTab(
{
tabKind: ViewModels.CollectionTabKind.Query,
title: title,
tabPath: "",
collection: this,
node: this,
queryText: queryText,
partitionKey: collection.partitionKey,
onLoadStartKey: startKey,
},
{ container: this.container }
)
);
}
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source;
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
const title = "Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: title,
});
const newMongoQueryTab: NewMongoQueryTab = new NewMongoQueryTab(
{
tabKind: ViewModels.CollectionTabKind.Query,
title: title,
tabPath: "",
collection: this,
node: this,
partitionKey: collection.partitionKey,
onLoadStartKey: startKey,
},
{
container: this.container,
viewModelcollection: this,
}
);
useTabs.getState().activateNewTab(newMongoQueryTab);
}
public onNewGraphClick() {
const id: number = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Graph).length + 1;
const title: string = "Graph Query " + id;
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: title,
});
const graphTab: GraphTab = new GraphTab({
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.Graph,
node: this,
title: title,
tabPath: "",
collection: this,
masterKey: userContext.masterKey || "",
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
collectionId: this.id(),
databaseId: this.databaseId,
isTabsContentExpanded: this.container.isTabsContentExpanded,
onLoadStartKey: startKey,
});
useTabs.getState().activateNewTab(graphTab);
}
public onNewMongoShellClick() {
const mongoShellTabs = useTabs.getState().getTabs(ViewModels.CollectionTabKind.MongoShell) as NewMongoShellTab[];
let index = 1;
if (mongoShellTabs.length > 0) {
index = mongoShellTabs[mongoShellTabs.length - 1].index + 1;
}
const mongoShellTab: NewMongoShellTab = new NewMongoShellTab(
{
tabKind: ViewModels.CollectionTabKind.MongoShell,
title: "Shell " + index,
tabPath: "",
collection: this,
node: this,
index: index,
},
{
container: this.container,
}
);
useTabs.getState().activateNewTab(mongoShellTab);
}
public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) {
StoredProcedure.create(source, event);
}
public onNewUserDefinedFunctionClick(source: ViewModels.Collection) {
UserDefinedFunction.create(source);
}
public onNewTriggerClick(source: ViewModels.Collection, event: MouseEvent) {
Trigger.create(source, event);
}
public createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure {
const node = new StoredProcedure(this.container, this, data);
useSelectedNode.getState().setSelectedNode(node);
this.children.push(node);
return node;
}
public createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction {
const node = new UserDefinedFunction(this.container, this, data);
useSelectedNode.getState().setSelectedNode(node);
this.children.push(node);
return node;
}
public createTriggerNode(data: TriggerDefinition & Resource): Trigger {
const node = new Trigger(this.container, this, data);
useSelectedNode.getState().setSelectedNode(node);
this.children.push(node);
return node;
}
public findStoredProcedureWithId(sprocId: string): StoredProcedure {
return _.find(this.storedProcedures(), (storedProcedure: StoredProcedure) => storedProcedure.id() === sprocId);
}
public findTriggerWithId(triggerId: string): Trigger {
return _.find(this.triggers(), (trigger: Trigger) => trigger.id() === triggerId);
}
public findUserDefinedFunctionWithId(userDefinedFunctionId: string): UserDefinedFunction {
return _.find(
this.userDefinedFunctions(),
(userDefinedFunction: Trigger) => userDefinedFunction.id() === userDefinedFunctionId
);
}
public expandCollapseStoredProcedures() {
this.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
if (this.isStoredProceduresExpanded()) {
this.collapseStoredProcedures();
} else {
this.expandStoredProcedures();
}
useTabs
.getState()
.refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
);
}
public expandStoredProcedures() {
if (this.isStoredProceduresExpanded()) {
return;
}
this.loadStoredProcedures().then(
() => {
this.isStoredProceduresExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Stored procedures node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
},
(error) => {
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Failed, {
description: "Stored procedures node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: getErrorMessage(error),
});
}
);
}
public collapseStoredProcedures() {
if (!this.isStoredProceduresExpanded()) {
return;
}
this.isStoredProceduresExpanded(false);
TelemetryProcessor.trace(Action.CollapseTreeNode, ActionModifiers.Mark, {
description: "Stored procedures node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
}
public expandCollapseUserDefinedFunctions() {
this.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
if (this.isUserDefinedFunctionsExpanded()) {
this.collapseUserDefinedFunctions();
} else {
this.expandUserDefinedFunctions();
}
useTabs
.getState()
.refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
);
}
public expandUserDefinedFunctions() {
if (this.isUserDefinedFunctionsExpanded()) {
return;
}
this.loadUserDefinedFunctions().then(
() => {
this.isUserDefinedFunctionsExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "UDF node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
},
(error) => {
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Failed, {
description: "UDF node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: getErrorMessage(error),
});
}
);
}
public collapseUserDefinedFunctions() {
if (!this.isUserDefinedFunctionsExpanded()) {
return;
}
this.isUserDefinedFunctionsExpanded(false);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "UDF node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
}
public expandCollapseTriggers() {
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
if (this.isTriggersExpanded()) {
this.collapseTriggers();
} else {
this.expandTriggers();
}
useTabs
.getState()
.refreshActiveTab(
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
);
}
public expandTriggers() {
if (this.isTriggersExpanded()) {
return;
}
this.loadTriggers().then(
() => {
this.isTriggersExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Triggers node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
},
(error) => {
this.isTriggersExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Triggers node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: getErrorMessage(error),
});
}
);
}
public collapseTriggers() {
if (!this.isTriggersExpanded()) {
return;
}
this.isTriggersExpanded(false);
TelemetryProcessor.trace(Action.CollapseTreeNode, ActionModifiers.Mark, {
description: "Triggers node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
}
public loadStoredProcedures(): Promise<any> {
return readStoredProcedures(this.databaseId, this.id()).then((storedProcedures) => {
const storedProceduresNodes: ViewModels.TreeNode[] = storedProcedures.map(
(storedProcedure) => new StoredProcedure(this.container, this, storedProcedure)
);
const otherNodes = this.children().filter((node) => node.nodeKind !== "StoredProcedure");
const allNodes = otherNodes.concat(storedProceduresNodes);
this.children(allNodes);
});
}
public loadUserDefinedFunctions(): Promise<any> {
return readUserDefinedFunctions(this.databaseId, this.id()).then((userDefinedFunctions) => {
const userDefinedFunctionsNodes: ViewModels.TreeNode[] = userDefinedFunctions.map(
(udf) => new UserDefinedFunction(this.container, this, udf)
);
const otherNodes = this.children().filter((node) => node.nodeKind !== "UserDefinedFunction");
const allNodes = otherNodes.concat(userDefinedFunctionsNodes);
this.children(allNodes);
});
}
public loadTriggers(): Promise<any> {
return readTriggers(this.databaseId, this.id()).then((triggers) => {
const triggerNodes: ViewModels.TreeNode[] = triggers.map(
(trigger: SqlTriggerResource | TriggerDefinition) => new Trigger(this.container, this, trigger)
);
const otherNodes = this.children().filter((node) => node.nodeKind !== "Trigger");
const allNodes = otherNodes.concat(triggerNodes);
this.children(allNodes);
});
}
public onDragOver(source: Collection, event: { originalEvent: DragEvent }) {
event.originalEvent.stopPropagation();
event.originalEvent.preventDefault();
}
public onDrop(source: Collection, event: { originalEvent: DragEvent }) {
event.originalEvent.stopPropagation();
event.originalEvent.preventDefault();
this.uploadFiles(event.originalEvent.dataTransfer.files);
}
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
if (!this.container) {
return undefined;
}
try {
const notifications: DataModels.Notification[] = await fetchPortalNotifications();
if (!notifications || notifications.length === 0) {
return undefined;
}
return _.find(notifications, (notification: DataModels.Notification) => {
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
return (
notification.kind === "message" &&
notification.collectionName === this.id() &&
notification.description &&
throughputUpdateRegExp.test(notification.description)
);
});
} catch (error) {
Logger.logError(
JSON.stringify({
error: getErrorMessage(error),
accountName: userContext?.databaseAccount,
databaseName: this.databaseId,
collectionName: this.id(),
}),
"Settings tree node"
);
return undefined;
}
}
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
return { data };
}
private uploadFile(file: File): Promise<UploadDetailsRecord> {
const reader = new FileReader();
const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => {
const fileData: string = evt.target.result;
this._createDocumentsFromFile(file.name, fileData).then((record) => resolve(record));
};
const onerror = (resolve: (value: UploadDetailsRecord) => void, evt: ProgressEvent): void => {
resolve({
fileName: file.name,
numSucceeded: 0,
numThrottled: 0,
numFailed: 1,
errors: [(evt as any).error.message],
});
};
return new Promise<UploadDetailsRecord>((resolve) => {
reader.onload = onload.bind(this, resolve);
reader.onerror = onerror.bind(this, resolve);
reader.readAsText(file);
});
}
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
const record: UploadDetailsRecord = {
fileName: fileName,
numSucceeded: 0,
numFailed: 0,
numThrottled: 0,
errors: [],
};
try {
const parsedContent = JSON.parse(documentContent);
if (Array.isArray(parsedContent)) {
const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
const chunkedContent = Array.from({ length: Math.ceil(parsedContent.length / chunkSize) }, (_, index) =>
parsedContent.slice(index * chunkSize, index * chunkSize + chunkSize)
);
for (const chunk of chunkedContent) {
let retryAttempts = 0;
let chunkComplete = false;
let documentsToAttempt = chunk;
while (retryAttempts < 10 && !chunkComplete) {
const responses = await bulkCreateDocument(this, documentsToAttempt);
const attemptedDocuments = [...documentsToAttempt];
documentsToAttempt = [];
responses.forEach((response, index) => {
if (response.statusCode === 201) {
record.numSucceeded++;
} else if (response.statusCode === 429) {
documentsToAttempt.push(attemptedDocuments[index]);
} else {
record.numFailed++;
}
});
if (documentsToAttempt.length === 0) {
chunkComplete = true;
break;
}
logConsoleInfo(
`${documentsToAttempt.length} document creations were throttled. Waiting ${retryAttempts} seconds and retrying throttled documents`
);
retryAttempts++;
await sleep(retryAttempts);
}
}
} else {
await createDocument(this, parsedContent);
record.numSucceeded++;
}
return record;
} catch (error) {
record.numFailed++;
record.errors = [...record.errors, error.message];
return record;
}
}
/**
* Top-level method that will open the correct tab type depending on account API
*/
public openTab(): void {
if (userContext.apiType === "Tables") {
this.onTableEntitiesClick();
return;
} else if (userContext.apiType === "Cassandra") {
this.onTableEntitiesClick();
return;
} else if (userContext.apiType === "Gremlin") {
this.onGraphDocumentsClick();
return;
} else if (userContext.apiType === "Mongo") {
this.onMongoDBDocumentsClick();
return;
}
this.onDocumentDBDocumentsClick();
}
/**
* Get correct collection label depending on account API
*/
public getLabel(): string {
if (userContext.apiType === "Tables") {
return "Entities";
} else if (userContext.apiType === "Cassandra") {
return "Rows";
} else if (userContext.apiType === "Gremlin") {
return "Graph";
} else if (userContext.apiType === "Mongo") {
return "Documents";
}
return "Items";
}
public getDatabase(): ViewModels.Database {
return useDatabases.getState().findDatabaseWithId(this.databaseId);
}
public async loadOffer(): Promise<void> {
if (!this.isOfferRead && !isServerlessAccount() && !this.offer()) {
const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, {
databaseName: this.databaseId,
collectionName: this.id(),
});
const params: DataModels.ReadCollectionOfferParams = {
collectionId: this.id(),
collectionResourceId: this.self,
databaseId: this.databaseId,
};
try {
this.offer(await readCollectionOffer(params));
this.usageSizeInKB(await getCollectionUsageSizeInKB(this.databaseId, this.id()));
this.isOfferRead = true;
TelemetryProcessor.traceSuccess(
Action.LoadOffers,
{
databaseName: this.databaseId,
collectionName: this.id(),
},
startKey
);
} catch (error) {
TelemetryProcessor.traceFailure(
Action.LoadOffers,
{
databaseName: this.databaseId,
collectionName: this.id(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
throw error;
}
}
}
}
function sleep(seconds: number) {
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}