import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; import * as ko from "knockout"; import * as _ from "underscore"; import UploadWorker from "worker-loader!../../workers/upload"; import { AuthType } from "../../AuthType"; import * as Constants from "../../Common/Constants"; import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures"; import { readTriggers } from "../../Common/dataAccess/readTriggers"; import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions"; import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer"; import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize"; import * as Logger from "../../Common/Logger"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient"; import ConflictsTab from "../Tabs/ConflictsTab"; import DocumentsTab from "../Tabs/DocumentsTab"; import GraphTab from "../Tabs/GraphTab"; import MongoDocumentsTab from "../Tabs/MongoDocumentsTab"; import MongoQueryTab from "../Tabs/MongoQueryTab"; import MongoShellTab from "../Tabs/MongoShellTab"; import QueryTab from "../Tabs/QueryTab"; import QueryTablesTab from "../Tabs/QueryTablesTab"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import ConflictId from "./ConflictId"; import DocumentId from "./DocumentId"; import StoredProcedure from "./StoredProcedure"; import Trigger from "./Trigger"; import UserDefinedFunction from "./UserDefinedFunction"; import { configContext, Platform } from "../../ConfigContext"; import Explorer from "../Explorer"; import { userContext } from "../../UserContext"; import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { createDocument } from "../../Common/dataAccess/createDocument"; 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 partitionKeyPropertyHeader: string; public partitionKeyProperty: string; public id: ko.Observable; public defaultTtl: ko.Observable; public indexingPolicy: ko.Observable; public uniqueKeyPolicy: DataModels.UniqueKeyPolicy; public usageSizeInKB: ko.Observable; public offer: ko.Observable; public conflictResolutionPolicy: ko.Observable; public changeFeedPolicy: ko.Observable; public partitions: ko.Computed; public throughput: ko.Computed; public rawDataModel: DataModels.Collection; public analyticalStorageTtl: ko.Observable; public schema: DataModels.ISchema; public requestSchema: () => void; public geospatialConfig: ko.Observable; // TODO move this to API customization class public cassandraKeys: CassandraTableKeys; public cassandraSchema: CassandraTableKey[]; public documentIds: ko.ObservableArray; public children: ko.ObservableArray; public storedProcedures: ko.Computed; public userDefinedFunctions: ko.Computed; public triggers: ko.Computed; public showStoredProcedures: ko.Observable; public showTriggers: ko.Observable; public showUserDefinedFunctions: ko.Observable; public showConflicts: ko.Observable; public selectedDocumentContent: ViewModels.Editable; public selectedSubnodeKind: ko.Observable; public focusedSubnodeKind: ko.Observable; public isCollectionExpanded: ko.Observable; public isStoredProceduresExpanded: ko.Observable; public isUserDefinedFunctionsExpanded: ko.Observable; public isTriggersExpanded: ko.Observable; public documentsFocused: ko.Observable; public settingsFocused: ko.Observable; public storedProceduresFocused: ko.Observable; public userDefinedFunctionsFocused: ko.Observable; public triggersFocused: ko.Observable; 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(data.changeFeedPolicy); this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl); this.schema = data.schema; this.requestSchema = data.requestSchema; this.geospatialConfig = ko.observable(data.geospatialConfig); // TODO fix this to only replace non-excaped single quotes this.partitionKeyProperty = (this.partitionKey && this.partitionKey.paths && this.partitionKey.paths.length && this.partitionKey.paths.length > 0 && this.partitionKey.paths[0].replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")) || null; this.partitionKeyPropertyHeader = (this.partitionKey && this.partitionKey.paths && this.partitionKey.paths.length > 0 && this.partitionKey.paths[0]) || null; if (!!container.isPreferredApiMongoDB() && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) { this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, ""); } // TODO #10738269 : Add this logic in a derived class for Mongo if ( !!container.isPreferredApiMongoDB() && this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1 ) { // From $v.shard.$v.key.$v > shard.key this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty; } this.documentIds = ko.observableArray([]); this.isCollectionExpanded = ko.observable(false); this.selectedSubnodeKind = ko.observable(); this.focusedSubnodeKind = ko.observable(); this.documentsFocused = ko.observable(); this.documentsFocused.subscribe((focus) => { console.log("Focus set on Documents: " + focus); this.focusedSubnodeKind(ViewModels.CollectionTabKind.Documents); }); this.settingsFocused = ko.observable(false); this.settingsFocused.subscribe((focus) => { this.focusedSubnodeKind(ViewModels.CollectionTabKind.Settings); }); this.storedProceduresFocused = ko.observable(false); this.storedProceduresFocused.subscribe((focus) => { this.focusedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures); }); this.userDefinedFunctionsFocused = ko.observable(false); this.userDefinedFunctionsFocused.subscribe((focus) => { this.focusedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions); }); this.triggersFocused = ko.observable(false); this.triggersFocused.subscribe((focus) => { this.focusedSubnodeKind(ViewModels.CollectionTabKind.Triggers); }); this.children = ko.observableArray([]); this.storedProcedures = ko.computed(() => { return this.children() .filter((node) => node.nodeKind === "StoredProcedure") .map((node) => node); }); this.userDefinedFunctions = ko.computed(() => { return this.children() .filter((node) => node.nodeKind === "UserDefinedFunction") .map((node) => node); }); this.triggers = ko.computed(() => { return this.children() .filter((node) => node.nodeKind === "Trigger") .map((node) => node); }); const showScriptsMenus: boolean = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph(); this.showStoredProcedures = ko.observable(showScriptsMenus); this.showTriggers = ko.observable(showScriptsMenus); this.showUserDefinedFunctions = ko.observable(showScriptsMenus); this.showConflicts = ko.observable( container && container.databaseAccount && container.databaseAccount() && container.databaseAccount().properties && container.databaseAccount().properties.enableMultipleWriteLocations && data && !!data.conflictResolutionPolicy ); this.isStoredProceduresExpanded = ko.observable(false); this.isUserDefinedFunctionsExpanded = ko.observable(false); this.isTriggersExpanded = ko.observable(false); } public expandCollapseCollection() { this.container.selectedNode(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(); } this.container.onUpdateTabsButtons([]); this.container.tabsManager.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() { this.container.selectedNode(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[] = this.container.tabsManager.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) { this.container.tabsManager.activateTab(documentsTab); } else { const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { databaseName: this.databaseId, collectionName: this.id(), dataExplorerArea: Constants.Areas.Tab, tabTitle: "Items", }); this.documentIds([]); documentsTab = new DocumentsTab({ partitionKey: this.partitionKey, documentIds: ko.observableArray([]), tabKind: ViewModels.CollectionTabKind.Documents, title: "Items", isActive: ko.observable(false), collection: this, node: this, tabPath: `${this.databaseId}>${this.id()}>Documents`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/documents`, onLoadStartKey: startKey, onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(documentsTab); } } public onConflictsClick() { this.container.selectedNode(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[] = this.container.tabsManager.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) { this.container.tabsManager.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([]), tabKind: ViewModels.CollectionTabKind.Conflicts, title: "Conflicts", isActive: ko.observable(false), collection: this, node: this, tabPath: `${this.databaseId}>${this.id()}>Conflicts`, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/conflicts`, onLoadStartKey: startKey, onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(conflictsTab); } } public onTableEntitiesClick() { this.container.selectedNode(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 (this.container.isPreferredApiCassandra() && !this.cassandraKeys) { (this.container.tableDataClient).getTableKeys(this).then((keys: CassandraTableKeys) => { this.cassandraKeys = keys; }); } const queryTablesTabs: QueryTablesTab[] = this.container.tabsManager.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) { this.container.tabsManager.activateTab(queryTablesTab); } else { this.documentIds([]); let title = `Entities`; if (this.container.isPreferredApiCassandra()) { 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, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/entities`, isActive: ko.observable(false), onLoadStartKey: startKey, onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(queryTablesTab); } } public onGraphDocumentsClick() { this.container.selectedNode(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[] = this.container.tabsManager.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) { this.container.tabsManager.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.partitionKeyProperty, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`, collectionId: this.id(), isActive: ko.observable(false), databaseId: this.databaseId, isTabsContentExpanded: this.container.isTabsContentExpanded, onLoadStartKey: startKey, onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(graphTab); } } public onMongoDBDocumentsClick = () => { this.container.selectedNode(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[] = this.container.tabsManager.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) { this.container.tabsManager.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, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoDocuments`, isActive: ko.observable(false), onLoadStartKey: startKey, onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(mongoDocumentsTab); } }; public onSettingsClick = async (): Promise => { this.container.selectedNode(this); 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 = this.container.tabsManager.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, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/settings`, isActive: ko.observable(false), onUpdateTabsButtons: this.container.onUpdateTabsButtons, }; 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); this.container.tabsManager.activateNewTab(settingsTabV2); } else { this.container.tabsManager.activateTab(settingsTabV2); } }; public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) { const collection: ViewModels.Collection = source.collection || source; const id = this.container.tabsManager.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 queryTab: QueryTab = new QueryTab({ tabKind: ViewModels.CollectionTabKind.Query, title: title, tabPath: "", collection: this, node: this, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`, isActive: ko.observable(false), queryText: queryText, partitionKey: collection.partitionKey, onLoadStartKey: startKey, onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(queryTab); } public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) { const collection: ViewModels.Collection = source.collection || source; const id = this.container.tabsManager.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 mongoQueryTab: MongoQueryTab = new MongoQueryTab({ tabKind: ViewModels.CollectionTabKind.Query, title: title, tabPath: "", collection: this, node: this, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`, isActive: ko.observable(false), partitionKey: collection.partitionKey, onLoadStartKey: startKey, onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(mongoQueryTab); } public onNewGraphClick() { const id: number = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Graph).length + 1; const title: string = "Graph Query " + id; const startKey: number = TelemetryProcessor.traceStart(Action.Tab, { 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.partitionKeyProperty, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`, collectionId: this.id(), isActive: ko.observable(false), databaseId: this.databaseId, isTabsContentExpanded: this.container.isTabsContentExpanded, onLoadStartKey: startKey, onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(graphTab); } public onNewMongoShellClick() { const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.MongoShell).length + 1; const mongoShellTab: MongoShellTab = new MongoShellTab({ tabKind: ViewModels.CollectionTabKind.MongoShell, title: "Shell " + id, tabPath: "", collection: this, node: this, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoShell`, isActive: ko.observable(false), onUpdateTabsButtons: this.container.onUpdateTabsButtons, }); this.container.tabsManager.activateNewTab(mongoShellTab); } public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) { StoredProcedure.create(source, event); } public onNewUserDefinedFunctionClick(source: ViewModels.Collection, event: MouseEvent) { UserDefinedFunction.create(source, event); } 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); this.container.selectedNode(node); this.children.push(node); return node; } public createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction { const node = new UserDefinedFunction(this.container, this, data); this.container.selectedNode(node); this.children.push(node); return node; } public createTriggerNode(data: TriggerDefinition & Resource): Trigger { const node = new Trigger(this.container, this, data); this.container.selectedNode(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(); } this.container.tabsManager.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(); } this.container.tabsManager.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(); } this.container.tabsManager.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 { 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 { 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 { return readTriggers(this.databaseId, this.id()).then((triggers) => { const triggerNodes: ViewModels.TreeNode[] = triggers.map((trigger) => 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 uploadFiles = (fileList: FileList): Promise => { // TODO: right now web worker is not working with AAD flow. Use main thread for upload for now until we have backend upload capability if (configContext.platform === Platform.Hosted && userContext.authType === AuthType.AAD) { return this._uploadFilesCors(fileList); } const documentUploader: Worker = new UploadWorker(); let inProgressNotificationId: string = ""; if (!fileList || fileList.length === 0) { return Promise.reject("No files specified"); } const onmessage = (resolve: (value: UploadDetails) => void, reject: (reason: any) => void, event: MessageEvent) => { const numSuccessful: number = event.data.numUploadsSuccessful; const numFailed: number = event.data.numUploadsFailed; const runtimeError: string = event.data.runtimeError; const uploadDetails: UploadDetails = event.data.uploadDetails; NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId); documentUploader.terminate(); if (!!runtimeError) { reject(runtimeError); } else if (numSuccessful === 0) { // all uploads failed NotificationConsoleUtils.logConsoleError(`Failed to upload all documents to container ${this.id()}`); } else if (numFailed > 0) { NotificationConsoleUtils.logConsoleError( `Failed to upload ${numFailed} of ${numSuccessful + numFailed} documents to container ${this.id()}` ); } else { NotificationConsoleUtils.logConsoleInfo( `Successfully uploaded all ${numSuccessful} documents to container ${this.id()}` ); } this._logUploadDetailsInConsole(uploadDetails); resolve(uploadDetails); }; function onerror(reject: (reason: any) => void, event: ErrorEvent) { documentUploader.terminate(); reject(event.error); } const uploaderMessage: StartUploadMessageParams = { files: fileList, documentClientParams: { databaseId: this.databaseId, containerId: this.id(), masterKey: userContext.masterKey, endpoint: userContext.endpoint, accessToken: userContext.accessToken, platform: configContext.platform, databaseAccount: userContext.databaseAccount, }, }; return new Promise((resolve, reject) => { documentUploader.onmessage = onmessage.bind(null, resolve, reject); documentUploader.onerror = onerror.bind(null, reject); documentUploader.postMessage(uploaderMessage); inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, `Uploading and creating documents in container ${this.id()}` ); }); }; public async getPendingThroughputSplitNotification(): Promise { 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: this.container && this.container.databaseAccount(), databaseName: this.databaseId, collectionName: this.id(), }), "Settings tree node" ); return undefined; } } private async _uploadFilesCors(files: FileList): Promise { const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file))); return { data }; } private _uploadFile(file: File): Promise { 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, numFailed: 1, errors: [(evt as any).error.message], }); }; return new Promise((resolve) => { reader.onload = onload.bind(this, resolve); reader.onerror = onerror.bind(this, resolve); reader.readAsText(file); }); } private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise { const record: UploadDetailsRecord = { fileName: fileName, numSucceeded: 0, numFailed: 0, errors: [], }; try { const content = JSON.parse(documentContent); if (Array.isArray(content)) { await Promise.all( content.map(async (documentContent) => { await createDocument(this, documentContent); record.numSucceeded++; }) ); } else { await createDocument(this, documentContent); record.numSucceeded++; } return record; } catch (error) { record.numFailed++; record.errors = [...record.errors, error.message]; return record; } } private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void { const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data; const numFiles: number = uploadDetailsRecords.length; const stackTraceLimit: number = 100; let stackTraceCount: number = 0; let currentFileIndex = 0; while (stackTraceCount < stackTraceLimit && currentFileIndex < numFiles) { const errors: string[] = uploadDetailsRecords[currentFileIndex].errors; for (let i = 0; i < errors.length; i++) { if (stackTraceCount >= stackTraceLimit) { break; } NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, `Document creation error for container ${this.id()} - file ${ uploadDetailsRecords[currentFileIndex].fileName }: ${errors[i]}` ); stackTraceCount++; } currentFileIndex++; } uploadDetailsRecords.forEach((record: UploadDetailsRecord) => { const consoleDataType: ConsoleDataType = record.numFailed > 0 ? ConsoleDataType.Error : ConsoleDataType.Info; NotificationConsoleUtils.logConsoleMessage( consoleDataType, `Item creation summary for container ${this.id()} - file ${record.fileName}: ${ record.numSucceeded } items created, ${record.numFailed} errors` ); }); } /** * Top-level method that will open the correct tab type depending on account API */ public openTab(): void { if (this.container.isPreferredApiTable()) { this.onTableEntitiesClick(); return; } else if (this.container.isPreferredApiCassandra()) { this.onTableEntitiesClick(); return; } else if (this.container.isPreferredApiGraph()) { this.onGraphDocumentsClick(); return; } else if (this.container.isPreferredApiMongoDB()) { this.onMongoDBDocumentsClick(); return; } this.onDocumentDBDocumentsClick(); } /** * Get correct collection label depending on account API */ public getLabel(): string { if (this.container.isPreferredApiTable()) { return "Entities"; } else if (this.container.isPreferredApiCassandra()) { return "Rows"; } else if (this.container.isPreferredApiGraph()) { return "Graph"; } else if (this.container.isPreferredApiMongoDB()) { return "Documents"; } return "Items"; } public getDatabase(): ViewModels.Database { return this.container.findDatabaseWithId(this.databaseId); } public async loadOffer(): Promise { if (!this.container.isServerlessEnabled() && !this.offer()) { this.container.isRefreshingExplorer(true); 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())); 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; } finally { this.container.isRefreshingExplorer(false); } } } }