From 8c40df0fa106a2d272b1ec08ac39d5e93edf4af0 Mon Sep 17 00:00:00 2001 From: Deborah Chen Date: Fri, 15 Jan 2021 15:15:15 -0800 Subject: [PATCH] Adding in experimentation for autoscale test (#345) * Adding autoscale flight info * Add flight info to cassandra collection pane * Add telemetry for autoscale toggle on/off in create resource blade and scale/settings * Run formatting and add expected properties to test file * removing empty line * Updating to pass unit tests Co-authored-by: Steve Faulkner --- src/Common/Constants.ts | 871 +-- .../SettingsSubComponents/ScaleComponent.tsx | 2 + ...roughputInputAutoPilotV3Component.test.tsx | 2 + .../ThroughputInputAutoPilotV3Component.tsx | 19 +- .../ScaleComponent.test.tsx.snap | 2 + .../SettingsComponent.test.tsx.snap | 4 + .../ThroughputInputComponentAutoPilotV3.ts | 13 + src/Explorer/Explorer.ts | 6092 +++++++++-------- src/Explorer/Panes/AddCollectionPane.ts | 4 +- src/Explorer/Panes/AddDatabasePane.ts | 2 +- .../Panes/CassandraAddCollectionPane.ts | 4 +- src/Shared/Telemetry/TelemetryConstants.ts | 5 +- 12 files changed, 3535 insertions(+), 3485 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 48142e959..25b1c2b2d 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -1,435 +1,436 @@ -import { HashMap } from "./HashMap"; - -export class AuthorizationEndpoints { - public static arm: string = "https://management.core.windows.net/"; - public static common: string = "https://login.windows.net/"; -} - -export class CodeOfConductEndpoints { - public static privacyStatement: string = "https://aka.ms/ms-privacy-policy"; - public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct"; - public static termsOfUse: string = "https://aka.ms/ms-terms-of-use"; -} - -export class EndpointsRegex { - public static readonly cassandra = [ - "AccountEndpoint=(.*).cassandra.cosmosdb.azure.com", - "HostName=(.*).cassandra.cosmos.azure.com" - ]; - public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com"; - public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com"; - public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com"; - public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com"; -} - -export class ApiEndpoints { - public static runtimeProxy: string = "/api/RuntimeProxy"; - public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy"; -} - -export class ServerIds { - public static localhost: string = "localhost"; - public static blackforest: string = "blackforest"; - public static fairfax: string = "fairfax"; - public static mooncake: string = "mooncake"; - public static productionPortal: string = "prod"; - public static dev: string = "dev"; -} - -export class ArmApiVersions { - public static readonly documentDB: string = "2015-11-06"; - public static readonly arcadia: string = "2019-06-01-preview"; - public static readonly arcadiaLivy: string = "2019-11-01-preview"; - public static readonly arm: string = "2015-11-01"; - public static readonly armFeatures: string = "2014-08-01-preview"; - public static readonly publicVersion = "2020-04-01"; -} - -export class ArmResourceTypes { - public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces"; - public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces"; -} - -export class BackendDefaults { - public static partitionKeyKind: string = "Hash"; - public static singlePartitionStorageInGb: string = "10"; - public static multiPartitionStorageInGb: string = "100"; - public static maxChangeFeedRetentionDuration: number = 10; - public static partitionKeyVersion = 2; -} - -export class ClientDefaults { - public static requestTimeoutMs: number = 60000; - public static portalCacheTimeoutMs: number = 10000; - public static errorNotificationTimeoutMs: number = 5000; - public static copyHelperTimeoutMs: number = 2000; - public static waitForDOMElementMs: number = 500; - public static cacheBustingTimeoutMs: number = - 10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/; - public static databaseThroughputIncreaseFactor: number = 100; - public static readonly arcadiaTokenRefreshInterval: number = - 20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/; - public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000; -} - -export class AccountKind { - public static DocumentDB: string = "DocumentDB"; - public static MongoDB: string = "MongoDB"; - public static Parse: string = "Parse"; - public static GlobalDocumentDB: string = "GlobalDocumentDB"; - public static Default: string = AccountKind.DocumentDB; -} - -export class CorrelationBackend { - public static Url: string = "https://aka.ms/cosmosdbanalytics"; -} - -export class DefaultAccountExperience { - public static DocumentDB: string = "DocumentDB"; - public static Graph: string = "Graph"; - public static MongoDB: string = "MongoDB"; - public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API"; - public static Table: string = "Table"; - public static Cassandra: string = "Cassandra"; - public static Default: string = DefaultAccountExperience.DocumentDB; -} - -export class CapabilityNames { - public static EnableTable: string = "EnableTable"; - public static EnableGremlin: string = "EnableGremlin"; - public static EnableCassandra: string = "EnableCassandra"; - public static EnableAutoScale: string = "EnableAutoScale"; - public static readonly EnableNotebooks: string = "EnableNotebooks"; - public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics"; - public static readonly EnableMongo: string = "EnableMongo"; - public static readonly EnableServerless: string = "EnableServerless"; -} - -export class Features { - public static readonly cosmosdb = "cosmosdb"; - public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy"; - public static readonly executeSproc = "dataexplorerexecutesproc"; - public static readonly hostedDataExplorer = "hosteddataexplorerenabled"; - public static readonly enableTtl = "enablettl"; - public static readonly enableNotebooks = "enablenotebooks"; - public static readonly enableGalleryPublish = "enablegallerypublish"; - public static readonly enableLinkInjection = "enablelinkinjection"; - public static readonly enableSpark = "enablespark"; - public static readonly livyEndpoint = "livyendpoint"; - public static readonly notebookServerUrl = "notebookserverurl"; - public static readonly notebookServerToken = "notebookservertoken"; - public static readonly notebookBasePath = "notebookbasepath"; - public static readonly canExceedMaximumValue = "canexceedmaximumvalue"; - public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput"; - public static readonly ttl90Days = "ttl90days"; - public static readonly enableRightPanelV2 = "enablerightpanelv2"; - public static readonly enableSchema = "enableschema"; - public static readonly enableSDKoperations = "enablesdkoperations"; - public static readonly showMinRUSurvey = "showminrusurvey"; -} - -// flight names returned from the portal are always lowercase -export class Flights { - public static readonly SettingsV2 = "settingsv2"; - public static readonly MongoIndexEditor = "mongoindexeditor"; - public static readonly MongoIndexing = "mongoindexing"; -} - -export class AfecFeatures { - public static readonly Spark = "spark-public-preview"; - public static readonly Notebooks = "sparknotebooks-public-preview"; - public static readonly StorageAnalytics = "storageanalytics-public-preview"; -} - -export class Spark { - public static readonly MaxWorkerCount = 10; - public static readonly SKUs: HashMap = new HashMap({ - "Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM", - "Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM", - "Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM", - "Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM", - "Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM", - "Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM", - "Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM" - }); -} - -export class TagNames { - public static defaultExperience: string = "defaultExperience"; -} - -export class MongoDBAccounts { - public static protocol: string = "https"; - public static defaultPort: string = "10255"; -} - -export enum MongoBackendEndpointType { - local, - remote -} - -// TODO: 435619 Add default endpoints per cloud and use regional only when available -export class CassandraBackend { - public static readonly createOrDeleteApi: string = "api/cassandra/createordelete"; - public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete"; - public static readonly queryApi: string = "api/cassandra"; - public static readonly guestQueryApi: string = "api/guest/cassandra"; - public static readonly keysApi: string = "api/cassandra/keys"; - public static readonly guestKeysApi: string = "api/guest/cassandra/keys"; - public static readonly schemaApi: string = "api/cassandra/schema"; - public static readonly guestSchemaApi: string = "api/guest/cassandra/schema"; -} - -export class Queries { - public static CustomPageOption: string = "custom"; - public static UnlimitedPageOption: string = "unlimited"; - public static itemsPerPage: number = 100; - public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions - - public static QueryEditorMinHeightRatio: number = 0.1; - public static QueryEditorMaxHeightRatio: number = 0.4; - public static readonly DefaultMaxDegreeOfParallelism = 6; -} - -export class SavedQueries { - public static readonly CollectionName: string = "___Query"; - public static readonly DatabaseName: string = "___Cosmos"; - public static readonly OfferThroughput: number = 400; - public static readonly PartitionKeyProperty: string = "id"; -} - -export class DocumentsGridMetrics { - public static DocumentsPerPage: number = 100; - public static IndividualRowHeight: number = 34; - public static BufferHeight: number = 28; - public static SplitterMinWidth: number = 200; - public static SplitterMaxWidth: number = 360; - - public static DocumentEditorMinWidthRatio: number = 0.2; - public static DocumentEditorMaxWidthRatio: number = 0.4; -} - -export class ExplorerMetrics { - public static SplitterMinWidth: number = 240; - public static SplitterMaxWidth: number = 400; - public static CollapsedResourceTreeWidth: number = 36; -} - -export class SplitterMetrics { - public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth; -} - -export class Areas { - public static ResourceTree: string = "Resource Tree"; - public static ContextualPane: string = "Contextual Pane"; - public static Tab: string = "Tab"; - public static ShareDialog: string = "Share Access Dialog"; - public static Notebook: string = "Notebook"; -} - -export class HttpHeaders { - public static activityId: string = "x-ms-activity-id"; - public static apiType: string = "x-ms-cosmos-apitype"; - public static authorization: string = "authorization"; - public static collectionIndexTransformationProgress: string = - "x-ms-documentdb-collection-index-transformation-progress"; - public static continuation: string = "x-ms-continuation"; - public static correlationRequestId: string = "x-ms-correlation-request-id"; - public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging"; - public static guestAccessToken: string = "x-ms-encrypted-auth-token"; - public static getReadOnlyKey: string = "x-ms-get-read-only-key"; - public static connectionString: string = "x-ms-connection-string"; - public static msDate: string = "x-ms-date"; - public static location: string = "Location"; - public static contentType: string = "Content-Type"; - public static offerReplacePending: string = "x-ms-offer-replace-pending"; - public static user: string = "x-ms-user"; - public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics"; - public static queryMetrics: string = "x-ms-documentdb-query-metrics"; - public static requestCharge: string = "x-ms-request-charge"; - public static resourceQuota: string = "x-ms-resource-quota"; - public static resourceUsage: string = "x-ms-resource-usage"; - public static retryAfterMs: string = "x-ms-retry-after-ms"; - public static scriptLogResults: string = "x-ms-documentdb-script-log-results"; - public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo"; - public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates"; - public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere"; - public static autoPilotThroughput = "autoscaleSettings"; - public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings"; - public static partitionKey: string = "x-ms-documentdb-partitionkey"; - public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput"; - public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot"; -} - -export class ApiType { - // Mapped to hexadecimal values in the backend - public static readonly MongoDB: number = 1; - public static readonly Gremlin: number = 2; - public static readonly Cassandra: number = 4; - public static readonly Table: number = 8; - public static readonly SQL: number = 16; -} - -export class HttpStatusCodes { - public static readonly OK: number = 200; - public static readonly Created: number = 201; - public static readonly Accepted: number = 202; - public static readonly NoContent: number = 204; - public static readonly NotModified: number = 304; - public static readonly Unauthorized: number = 401; - public static readonly Forbidden: number = 403; - public static readonly NotFound: number = 404; - public static readonly TooManyRequests: number = 429; - public static readonly Conflict: number = 409; - - public static readonly InternalServerError: number = 500; - public static readonly BadGateway: number = 502; - public static readonly ServiceUnavailable: number = 503; - public static readonly GatewayTimeout: number = 504; - - public static readonly RetryableStatusCodes: number[] = [ - HttpStatusCodes.TooManyRequests, - HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list - HttpStatusCodes.BadGateway, - HttpStatusCodes.ServiceUnavailable, - HttpStatusCodes.GatewayTimeout - ]; -} - -export class Urls { - public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback"; - public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration"; - public static freeTierInformation = "https://aka.ms/cosmos-free-tier"; - public static cosmosPricing = "https://aka.ms/azure-cosmos-db-pricing"; -} - -export class HashRoutePrefixes { - public static databases: string = "/dbs/{db_id}"; - public static collections: string = "/dbs/{db_id}/colls/{coll_id}"; - public static sprocHash: string = "/sprocs/"; - public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}"; - public static docs: string = HashRoutePrefixes.collections + "/docs/{doc_id}/"; - public static conflicts: string = HashRoutePrefixes.collections + "/conflicts"; - - public static databasesWithId(databaseId: string): string { - return this.databases.replace("{db_id}", databaseId).replace("/", ""); // strip the first slash since hasher adds it - } - - public static collectionsWithIds(databaseId: string, collectionId: string): string { - const transformedDatabasePrefix: string = this.collections.replace("{db_id}", databaseId); - - return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it - } - - public static sprocWithIds( - databaseId: string, - collectionId: string, - sprocId: string, - stripFirstSlash: boolean = true - ): string { - const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId); - - const transformedSprocRoute: string = transformedDatabasePrefix - .replace("{coll_id}", collectionId) - .replace("{sproc_id}", sprocId); - if (!!stripFirstSlash) { - return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it - } - - return transformedSprocRoute; - } - - public static conflictsWithIds(databaseId: string, collectionId: string) { - const transformedDatabasePrefix: string = this.conflicts.replace("{db_id}", databaseId); - - return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it; - } - - public static docsWithIds(databaseId: string, collectionId: string, docId: string) { - const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId); - - return transformedDatabasePrefix - .replace("{coll_id}", collectionId) - .replace("{doc_id}", docId) - .replace("/", ""); // strip the first slash since hasher adds it - } -} - -export class ConfigurationOverridesValues { - public static IsBsonSchemaV2: string = "true"; -} - -export class KeyCodes { - public static Space: number = 32; - public static Enter: number = 13; - public static Escape: number = 27; - public static UpArrow: number = 38; - public static DownArrow: number = 40; - public static LeftArrow: number = 37; - public static RightArrow: number = 39; - public static Tab: number = 9; -} - -// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values -export class NormalizedEventKey { - public static readonly Space = " "; - public static readonly Enter = "Enter"; - public static readonly Escape = "Escape"; - public static readonly UpArrow = "ArrowUp"; - public static readonly DownArrow = "ArrowDown"; - public static readonly LeftArrow = "ArrowLeft"; - public static readonly RightArrow = "ArrowRight"; -} - -export class TryCosmosExperience { - public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}"; - public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}"; - public static collectionsPerAccount: number = 3; - public static maxRU: number = 5000; - public static defaultRU: number = 3000; -} - -export class OfferVersions { - public static V1: string = "V1"; - public static V2: string = "V2"; -} - -export enum ConflictOperationType { - Replace = "replace", - Create = "create", - Delete = "delete" -} - -export const EmulatorMasterKey = - //[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] - "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; - -// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable -export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less"); - -export class Notebook { - public static readonly defaultBasePath = "./notebooks"; - public static readonly heartbeatDelayMs = 5000; - public static readonly kernelRestartInitialDelayMs = 1000; - public static readonly kernelRestartMaxDelayMs = 20000; - public static readonly autoSaveIntervalMs = 120000; -} - -export class SparkLibrary { - public static readonly nameMinLength = 3; - public static readonly nameMaxLength = 63; -} - -export class AnalyticalStorageTtl { - public static readonly Days90: number = 7776000; - public static readonly Infinite: number = -1; - public static readonly Disabled: number = 0; -} - -export class TerminalQueryParams { - public static readonly Terminal = "terminal"; - public static readonly Server = "server"; - public static readonly Token = "token"; - public static readonly SubscriptionId = "subscriptionId"; - public static readonly TerminalEndpoint = "terminalEndpoint"; -} +import { HashMap } from "./HashMap"; + +export class AuthorizationEndpoints { + public static arm: string = "https://management.core.windows.net/"; + public static common: string = "https://login.windows.net/"; +} + +export class CodeOfConductEndpoints { + public static privacyStatement: string = "https://aka.ms/ms-privacy-policy"; + public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct"; + public static termsOfUse: string = "https://aka.ms/ms-terms-of-use"; +} + +export class EndpointsRegex { + public static readonly cassandra = [ + "AccountEndpoint=(.*).cassandra.cosmosdb.azure.com", + "HostName=(.*).cassandra.cosmos.azure.com" + ]; + public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com"; + public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com"; + public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com"; + public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com"; +} + +export class ApiEndpoints { + public static runtimeProxy: string = "/api/RuntimeProxy"; + public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy"; +} + +export class ServerIds { + public static localhost: string = "localhost"; + public static blackforest: string = "blackforest"; + public static fairfax: string = "fairfax"; + public static mooncake: string = "mooncake"; + public static productionPortal: string = "prod"; + public static dev: string = "dev"; +} + +export class ArmApiVersions { + public static readonly documentDB: string = "2015-11-06"; + public static readonly arcadia: string = "2019-06-01-preview"; + public static readonly arcadiaLivy: string = "2019-11-01-preview"; + public static readonly arm: string = "2015-11-01"; + public static readonly armFeatures: string = "2014-08-01-preview"; + public static readonly publicVersion = "2020-04-01"; +} + +export class ArmResourceTypes { + public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces"; + public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces"; +} + +export class BackendDefaults { + public static partitionKeyKind: string = "Hash"; + public static singlePartitionStorageInGb: string = "10"; + public static multiPartitionStorageInGb: string = "100"; + public static maxChangeFeedRetentionDuration: number = 10; + public static partitionKeyVersion = 2; +} + +export class ClientDefaults { + public static requestTimeoutMs: number = 60000; + public static portalCacheTimeoutMs: number = 10000; + public static errorNotificationTimeoutMs: number = 5000; + public static copyHelperTimeoutMs: number = 2000; + public static waitForDOMElementMs: number = 500; + public static cacheBustingTimeoutMs: number = + 10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/; + public static databaseThroughputIncreaseFactor: number = 100; + public static readonly arcadiaTokenRefreshInterval: number = + 20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/; + public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000; +} + +export class AccountKind { + public static DocumentDB: string = "DocumentDB"; + public static MongoDB: string = "MongoDB"; + public static Parse: string = "Parse"; + public static GlobalDocumentDB: string = "GlobalDocumentDB"; + public static Default: string = AccountKind.DocumentDB; +} + +export class CorrelationBackend { + public static Url: string = "https://aka.ms/cosmosdbanalytics"; +} + +export class DefaultAccountExperience { + public static DocumentDB: string = "DocumentDB"; + public static Graph: string = "Graph"; + public static MongoDB: string = "MongoDB"; + public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API"; + public static Table: string = "Table"; + public static Cassandra: string = "Cassandra"; + public static Default: string = DefaultAccountExperience.DocumentDB; +} + +export class CapabilityNames { + public static EnableTable: string = "EnableTable"; + public static EnableGremlin: string = "EnableGremlin"; + public static EnableCassandra: string = "EnableCassandra"; + public static EnableAutoScale: string = "EnableAutoScale"; + public static readonly EnableNotebooks: string = "EnableNotebooks"; + public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics"; + public static readonly EnableMongo: string = "EnableMongo"; + public static readonly EnableServerless: string = "EnableServerless"; +} + +export class Features { + public static readonly cosmosdb = "cosmosdb"; + public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy"; + public static readonly executeSproc = "dataexplorerexecutesproc"; + public static readonly hostedDataExplorer = "hosteddataexplorerenabled"; + public static readonly enableTtl = "enablettl"; + public static readonly enableNotebooks = "enablenotebooks"; + public static readonly enableGalleryPublish = "enablegallerypublish"; + public static readonly enableLinkInjection = "enablelinkinjection"; + public static readonly enableSpark = "enablespark"; + public static readonly livyEndpoint = "livyendpoint"; + public static readonly notebookServerUrl = "notebookserverurl"; + public static readonly notebookServerToken = "notebookservertoken"; + public static readonly notebookBasePath = "notebookbasepath"; + public static readonly canExceedMaximumValue = "canexceedmaximumvalue"; + public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput"; + public static readonly ttl90Days = "ttl90days"; + public static readonly enableRightPanelV2 = "enablerightpanelv2"; + public static readonly enableSchema = "enableschema"; + public static readonly enableSDKoperations = "enablesdkoperations"; + public static readonly showMinRUSurvey = "showminrusurvey"; +} + +// flight names returned from the portal are always lowercase +export class Flights { + public static readonly SettingsV2 = "settingsv2"; + public static readonly MongoIndexEditor = "mongoindexeditor"; + public static readonly AutoscaleTest = "autoscaletest"; + public static readonly MongoIndexing = "mongoindexing"; +} + +export class AfecFeatures { + public static readonly Spark = "spark-public-preview"; + public static readonly Notebooks = "sparknotebooks-public-preview"; + public static readonly StorageAnalytics = "storageanalytics-public-preview"; +} + +export class Spark { + public static readonly MaxWorkerCount = 10; + public static readonly SKUs: HashMap = new HashMap({ + "Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM", + "Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM", + "Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM", + "Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM", + "Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM", + "Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM", + "Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM" + }); +} + +export class TagNames { + public static defaultExperience: string = "defaultExperience"; +} + +export class MongoDBAccounts { + public static protocol: string = "https"; + public static defaultPort: string = "10255"; +} + +export enum MongoBackendEndpointType { + local, + remote +} + +// TODO: 435619 Add default endpoints per cloud and use regional only when available +export class CassandraBackend { + public static readonly createOrDeleteApi: string = "api/cassandra/createordelete"; + public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete"; + public static readonly queryApi: string = "api/cassandra"; + public static readonly guestQueryApi: string = "api/guest/cassandra"; + public static readonly keysApi: string = "api/cassandra/keys"; + public static readonly guestKeysApi: string = "api/guest/cassandra/keys"; + public static readonly schemaApi: string = "api/cassandra/schema"; + public static readonly guestSchemaApi: string = "api/guest/cassandra/schema"; +} + +export class Queries { + public static CustomPageOption: string = "custom"; + public static UnlimitedPageOption: string = "unlimited"; + public static itemsPerPage: number = 100; + public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions + + public static QueryEditorMinHeightRatio: number = 0.1; + public static QueryEditorMaxHeightRatio: number = 0.4; + public static readonly DefaultMaxDegreeOfParallelism = 6; +} + +export class SavedQueries { + public static readonly CollectionName: string = "___Query"; + public static readonly DatabaseName: string = "___Cosmos"; + public static readonly OfferThroughput: number = 400; + public static readonly PartitionKeyProperty: string = "id"; +} + +export class DocumentsGridMetrics { + public static DocumentsPerPage: number = 100; + public static IndividualRowHeight: number = 34; + public static BufferHeight: number = 28; + public static SplitterMinWidth: number = 200; + public static SplitterMaxWidth: number = 360; + + public static DocumentEditorMinWidthRatio: number = 0.2; + public static DocumentEditorMaxWidthRatio: number = 0.4; +} + +export class ExplorerMetrics { + public static SplitterMinWidth: number = 240; + public static SplitterMaxWidth: number = 400; + public static CollapsedResourceTreeWidth: number = 36; +} + +export class SplitterMetrics { + public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth; +} + +export class Areas { + public static ResourceTree: string = "Resource Tree"; + public static ContextualPane: string = "Contextual Pane"; + public static Tab: string = "Tab"; + public static ShareDialog: string = "Share Access Dialog"; + public static Notebook: string = "Notebook"; +} + +export class HttpHeaders { + public static activityId: string = "x-ms-activity-id"; + public static apiType: string = "x-ms-cosmos-apitype"; + public static authorization: string = "authorization"; + public static collectionIndexTransformationProgress: string = + "x-ms-documentdb-collection-index-transformation-progress"; + public static continuation: string = "x-ms-continuation"; + public static correlationRequestId: string = "x-ms-correlation-request-id"; + public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging"; + public static guestAccessToken: string = "x-ms-encrypted-auth-token"; + public static getReadOnlyKey: string = "x-ms-get-read-only-key"; + public static connectionString: string = "x-ms-connection-string"; + public static msDate: string = "x-ms-date"; + public static location: string = "Location"; + public static contentType: string = "Content-Type"; + public static offerReplacePending: string = "x-ms-offer-replace-pending"; + public static user: string = "x-ms-user"; + public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics"; + public static queryMetrics: string = "x-ms-documentdb-query-metrics"; + public static requestCharge: string = "x-ms-request-charge"; + public static resourceQuota: string = "x-ms-resource-quota"; + public static resourceUsage: string = "x-ms-resource-usage"; + public static retryAfterMs: string = "x-ms-retry-after-ms"; + public static scriptLogResults: string = "x-ms-documentdb-script-log-results"; + public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo"; + public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates"; + public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere"; + public static autoPilotThroughput = "autoscaleSettings"; + public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings"; + public static partitionKey: string = "x-ms-documentdb-partitionkey"; + public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput"; + public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot"; +} + +export class ApiType { + // Mapped to hexadecimal values in the backend + public static readonly MongoDB: number = 1; + public static readonly Gremlin: number = 2; + public static readonly Cassandra: number = 4; + public static readonly Table: number = 8; + public static readonly SQL: number = 16; +} + +export class HttpStatusCodes { + public static readonly OK: number = 200; + public static readonly Created: number = 201; + public static readonly Accepted: number = 202; + public static readonly NoContent: number = 204; + public static readonly NotModified: number = 304; + public static readonly Unauthorized: number = 401; + public static readonly Forbidden: number = 403; + public static readonly NotFound: number = 404; + public static readonly TooManyRequests: number = 429; + public static readonly Conflict: number = 409; + + public static readonly InternalServerError: number = 500; + public static readonly BadGateway: number = 502; + public static readonly ServiceUnavailable: number = 503; + public static readonly GatewayTimeout: number = 504; + + public static readonly RetryableStatusCodes: number[] = [ + HttpStatusCodes.TooManyRequests, + HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list + HttpStatusCodes.BadGateway, + HttpStatusCodes.ServiceUnavailable, + HttpStatusCodes.GatewayTimeout + ]; +} + +export class Urls { + public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback"; + public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration"; + public static freeTierInformation = "https://aka.ms/cosmos-free-tier"; + public static cosmosPricing = "https://aka.ms/azure-cosmos-db-pricing"; +} + +export class HashRoutePrefixes { + public static databases: string = "/dbs/{db_id}"; + public static collections: string = "/dbs/{db_id}/colls/{coll_id}"; + public static sprocHash: string = "/sprocs/"; + public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}"; + public static docs: string = HashRoutePrefixes.collections + "/docs/{doc_id}/"; + public static conflicts: string = HashRoutePrefixes.collections + "/conflicts"; + + public static databasesWithId(databaseId: string): string { + return this.databases.replace("{db_id}", databaseId).replace("/", ""); // strip the first slash since hasher adds it + } + + public static collectionsWithIds(databaseId: string, collectionId: string): string { + const transformedDatabasePrefix: string = this.collections.replace("{db_id}", databaseId); + + return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it + } + + public static sprocWithIds( + databaseId: string, + collectionId: string, + sprocId: string, + stripFirstSlash: boolean = true + ): string { + const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId); + + const transformedSprocRoute: string = transformedDatabasePrefix + .replace("{coll_id}", collectionId) + .replace("{sproc_id}", sprocId); + if (!!stripFirstSlash) { + return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it + } + + return transformedSprocRoute; + } + + public static conflictsWithIds(databaseId: string, collectionId: string) { + const transformedDatabasePrefix: string = this.conflicts.replace("{db_id}", databaseId); + + return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it; + } + + public static docsWithIds(databaseId: string, collectionId: string, docId: string) { + const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId); + + return transformedDatabasePrefix + .replace("{coll_id}", collectionId) + .replace("{doc_id}", docId) + .replace("/", ""); // strip the first slash since hasher adds it + } +} + +export class ConfigurationOverridesValues { + public static IsBsonSchemaV2: string = "true"; +} + +export class KeyCodes { + public static Space: number = 32; + public static Enter: number = 13; + public static Escape: number = 27; + public static UpArrow: number = 38; + public static DownArrow: number = 40; + public static LeftArrow: number = 37; + public static RightArrow: number = 39; + public static Tab: number = 9; +} + +// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values +export class NormalizedEventKey { + public static readonly Space = " "; + public static readonly Enter = "Enter"; + public static readonly Escape = "Escape"; + public static readonly UpArrow = "ArrowUp"; + public static readonly DownArrow = "ArrowDown"; + public static readonly LeftArrow = "ArrowLeft"; + public static readonly RightArrow = "ArrowRight"; +} + +export class TryCosmosExperience { + public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}"; + public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}"; + public static collectionsPerAccount: number = 3; + public static maxRU: number = 5000; + public static defaultRU: number = 3000; +} + +export class OfferVersions { + public static V1: string = "V1"; + public static V2: string = "V2"; +} + +export enum ConflictOperationType { + Replace = "replace", + Create = "create", + Delete = "delete" +} + +export const EmulatorMasterKey = + //[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] + "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + +// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable +export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less"); + +export class Notebook { + public static readonly defaultBasePath = "./notebooks"; + public static readonly heartbeatDelayMs = 5000; + public static readonly kernelRestartInitialDelayMs = 1000; + public static readonly kernelRestartMaxDelayMs = 20000; + public static readonly autoSaveIntervalMs = 120000; +} + +export class SparkLibrary { + public static readonly nameMinLength = 3; + public static readonly nameMaxLength = 63; +} + +export class AnalyticalStorageTtl { + public static readonly Days90: number = 7776000; + public static readonly Infinite: number = -1; + public static readonly Disabled: number = 0; +} + +export class TerminalQueryParams { + public static readonly Terminal = "terminal"; + public static readonly Server = "server"; + public static readonly Token = "token"; + public static readonly SubscriptionId = "subscriptionId"; + public static readonly TerminalEndpoint = "terminalEndpoint"; +} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index 373e03c4c..6f46b7cdf 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -165,6 +165,8 @@ export class ScaleComponent extends React.Component { private getThroughputInputComponent = (): JSX.Element => ( { const baseProps: ThroughputInputAutoPilotV3Props = { databaseAccount: {} as DataModels.DatabaseAccount, + databaseName: "test", + collectionName: "test", serverId: undefined, wasAutopilotOriginallySet: false, throughput: 100, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index c960cec0e..864cc9385 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -41,8 +41,13 @@ import { SubscriptionType } from "../../../../../Contracts/SubscriptionType"; import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils"; import { Features } from "../../../../../Common/Constants"; +import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor"; +import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants"; + export interface ThroughputInputAutoPilotV3Props { databaseAccount: DataModels.DatabaseAccount; + databaseName: string; + collectionName: string; serverId: string; throughput: number; throughputBaseline: number; @@ -447,7 +452,19 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< private onChoiceGroupChange = ( event?: React.FormEvent, option?: IChoiceGroupOption - ): void => this.props.onAutoPilotSelected(option.key === "true"); + ): void => { + this.props.onAutoPilotSelected(option.key === "true"); + TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, { + changedSelectedValueTo: + option.key === "true" ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff, + subscriptionId: userContext.subscriptionId, + databaseAccountName: this.props.databaseAccount?.name, + databaseName: this.props.databaseName, + collectionName: this.props.collectionName, + apiKind: userContext.defaultExperience, + dataExplorerArea: "Scale Tab V2" + }); + }; private minRUperGBSurvey = (): JSX.Element => { const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap index abba424b9..c5d2f18db 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/ScaleComponent.test.tsx.snap @@ -40,6 +40,8 @@ exports[`ScaleComponent renders with correct initial notification 1`] = ` > (); this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable(true); this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable(false); + this.isAutoPilotSelected.subscribe(value => { + TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, { + changedSelectedValueTo: value ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff, + databaseAccountName: userContext.databaseAccount?.name, + subscriptionId: userContext.subscriptionId, + apiKind: userContext.defaultExperience, + dataExplorerArea: "Scale Tab V1" + }); + }); + this.throughputAutoPilotRadioId = options.throughputAutoPilotRadioId; this.throughputProvisionedRadioId = options.throughputProvisionedRadioId; this.throughputModeRadioName = options.throughputModeRadioName; diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index 86c79b4c9..c424a8968 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -1,3043 +1,3049 @@ -import * as ComponentRegisterer from "./ComponentRegisterer"; -import * as Constants from "../Common/Constants"; -import * as DataModels from "../Contracts/DataModels"; -import * as ko from "knockout"; -import * as MostRecentActivity from "./MostRecentActivity/MostRecentActivity"; -import * as path from "path"; -import * as SharedConstants from "../Shared/Constants"; -import * as ViewModels from "../Contracts/ViewModels"; -import _ from "underscore"; -import AddCollectionPane from "./Panes/AddCollectionPane"; -import AddDatabasePane from "./Panes/AddDatabasePane"; -import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane"; -import AuthHeadersUtil from "../Platform/Hosted/Authorization"; -import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; -import Database from "./Tree/Database"; -import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; -import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane"; -import { readCollection } from "../Common/dataAccess/readCollection"; -import { readDatabases } from "../Common/dataAccess/readDatabases"; -import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; -import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; -import GraphStylingPane from "./Panes/GraphStylingPane"; -import hasher from "hasher"; -import NewVertexPane from "./Panes/NewVertexPane"; -import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; -import Q from "q"; -import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; -import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; -import TerminalTab from "./Tabs/TerminalTab"; -import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; -import { ActionContracts, MessageTypes } from "../Contracts/ExplorerContracts"; -import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager"; -import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker"; -import { AuthType } from "../AuthType"; -import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; -import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane"; -import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; -import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter"; -import { configContext, Platform, updateConfigContext } from "../ConfigContext"; -import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent"; -import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils"; -import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; -import { DialogComponentAdapter } from "./Controls/DialogReactComponent/DialogComponentAdapter"; -import { DialogProps, TextFieldProps } from "./Controls/DialogReactComponent/DialogComponent"; -import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane"; -import { ExplorerMetrics } from "../Common/Constants"; -import { ExplorerSettings } from "../Shared/ExplorerSettings"; -import { FileSystemUtil } from "./Notebook/FileSystemUtil"; -import { handleOpenAction } from "./OpenActions"; -import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; -import { IGalleryItem } from "../Juno/JunoClient"; -import { LoadQueryPane } from "./Panes/LoadQueryPane"; -import * as Logger from "../Common/Logger"; -import { sendMessage, sendCachedDataMessage, handleCachedDataMessage } from "../Common/MessageHandler"; -import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; -import { NotebookUtil } from "./Notebook/NotebookUtil"; -import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; -import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter"; -import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; -import { QueriesClient } from "../Common/QueriesClient"; -import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; -import { RenewAdHocAccessPane } from "./Panes/RenewAdHocAccessPane"; -import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; -import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; -import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; -import { RouteHandler } from "../RouteHandlers/RouteHandler"; -import { SaveQueryPane } from "./Panes/SaveQueryPane"; -import { SettingsPane } from "./Panes/SettingsPane"; -import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane"; -import { SplashScreenComponentAdapter } from "./SplashScreen/SplashScreenComponentApdapter"; -import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter"; -import { StringInputPane } from "./Panes/StringInputPane"; -import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane"; -import { TabsManager } from "./Tabs/TabsManager"; -import { UploadFilePane } from "./Panes/UploadFilePane"; -import { UploadItemsPane } from "./Panes/UploadItemsPane"; -import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter"; -import { ReactAdapter } from "../Bindings/ReactBindingHandler"; -import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils"; -import UserDefinedFunction from "./Tree/UserDefinedFunction"; -import StoredProcedure from "./Tree/StoredProcedure"; -import Trigger from "./Tree/Trigger"; -import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; -import TabsBase from "./Tabs/TabsBase"; -import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent"; -import { updateUserContext, userContext } from "../UserContext"; -import { stringToBlob } from "../Utils/BlobUtils"; -import { IChoiceGroupProps } from "office-ui-fabric-react"; -import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils"; -import { SubscriptionType } from "../Contracts/SubscriptionType"; - -BindingHandlersRegisterer.registerBindingHandlers(); -// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import -var tmp = ComponentRegisterer; - -enum ShareAccessToggleState { - ReadWrite, - Read -} - -interface AdHocAccessData { - readWriteUrl: string; - readUrl: string; -} - -export default class Explorer { - public flight: ko.Observable = ko.observable( - SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight - ); - - public addCollectionText: ko.Observable; - public addDatabaseText: ko.Observable; - public collectionTitle: ko.Observable; - public deleteCollectionText: ko.Observable; - public deleteDatabaseText: ko.Observable; - public collectionTreeNodeAltText: ko.Observable; - public refreshTreeTitle: ko.Observable; - public hasWriteAccess: ko.Observable; - public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth; - - public databaseAccount: ko.Observable; - public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; - public subscriptionType: ko.Observable; - public defaultExperience: ko.Observable; - public isPreferredApiDocumentDB: ko.Computed; - public isPreferredApiCassandra: ko.Computed; - public isPreferredApiMongoDB: ko.Computed; - public isPreferredApiGraph: ko.Computed; - public isPreferredApiTable: ko.Computed; - public isFixedCollectionWithSharedThroughputSupported: ko.Computed; - public isEnableMongoCapabilityPresent: ko.Computed; - public isServerlessEnabled: ko.Computed; - public isAccountReady: ko.Observable; - public canSaveQueries: ko.Computed; - public features: ko.Observable; - public serverId: ko.Observable; - public isTryCosmosDBSubscription: ko.Observable; - public queriesClient: QueriesClient; - public tableDataClient: TableDataClient; - public splitter: Splitter; - public mostRecentActivity: MostRecentActivity.MostRecentActivity; - - // Notification Console - public notificationConsoleData: ko.ObservableArray; - public isNotificationConsoleExpanded: ko.Observable; - - // Panes - public contextPanes: ContextualPaneBase[]; - - // Resource Tree - public databases: ko.ObservableArray; - public nonSystemDatabases: ko.Computed; - public selectedDatabaseId: ko.Computed; - public selectedCollectionId: ko.Computed; - public isLeftPaneExpanded: ko.Observable; - public selectedNode: ko.Observable; - public isRefreshingExplorer: ko.Observable; - private resourceTree: ResourceTreeAdapter; - - // Resource Token - public resourceTokenDatabaseId: ko.Observable; - public resourceTokenCollectionId: ko.Observable; - public resourceTokenCollection: ko.Observable; - public resourceTokenPartitionKey: ko.Observable; - public isAuthWithResourceToken: ko.Observable; - public isResourceTokenCollectionNodeSelected: ko.Computed; - private resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken; - - // Tabs - public isTabsContentExpanded: ko.Observable; - public galleryTab: any; - public notebookViewerTab: any; - public tabsManager: TabsManager; - - // Contextual panes - public addDatabasePane: AddDatabasePane; - public addCollectionPane: AddCollectionPane; - public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane; - public deleteDatabaseConfirmationPane: DeleteDatabaseConfirmationPane; - public graphStylingPane: GraphStylingPane; - public addTableEntityPane: AddTableEntityPane; - public editTableEntityPane: EditTableEntityPane; - public tableColumnOptionsPane: TableColumnOptionsPane; - public querySelectPane: QuerySelectPane; - public newVertexPane: NewVertexPane; - public cassandraAddCollectionPane: CassandraAddCollectionPane; - public settingsPane: SettingsPane; - public executeSprocParamsPane: ExecuteSprocParamsPane; - public renewAdHocAccessPane: RenewAdHocAccessPane; - public uploadItemsPane: UploadItemsPane; - public uploadItemsPaneAdapter: UploadItemsPaneAdapter; - public loadQueryPane: LoadQueryPane; - public saveQueryPane: ContextualPaneBase; - public browseQueriesPane: BrowseQueriesPane; - public uploadFilePane: UploadFilePane; - public stringInputPane: StringInputPane; - public setupNotebooksPane: SetupNotebooksPane; - public gitHubReposPane: ContextualPaneBase; - public publishNotebookPaneAdapter: ReactAdapter; - public copyNotebookPaneAdapter: ReactAdapter; - - // features - public isGalleryPublishEnabled: ko.Computed; - public isLinkInjectionEnabled: ko.Computed; - public isGitHubPaneEnabled: ko.Observable; - public isPublishNotebookPaneEnabled: ko.Observable; - public isCopyNotebookPaneEnabled: ko.Observable; - public isHostedDataExplorerEnabled: ko.Computed; - public isRightPanelV2Enabled: ko.Computed; - public isMongoIndexingEnabled: ko.Observable; - public canExceedMaximumValue: ko.Computed; - - public shouldShowShareDialogContents: ko.Observable; - public shareAccessData: ko.Observable; - public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise; - public renewTokenError: ko.Observable; - public tokenForRenewal: ko.Observable; - public shareAccessToggleState: ko.Observable; - public shareAccessUrl: ko.Observable; - public shareUrlCopyHelperText: ko.Observable; - public shareTokenCopyHelperText: ko.Observable; - public shouldShowDataAccessExpiryDialog: ko.Observable; - public shouldShowContextSwitchPrompt: ko.Observable; - public isSchemaEnabled: ko.Computed; - - // Notebooks - public isNotebookEnabled: ko.Observable; - public isNotebooksEnabledForAccount: ko.Observable; - public notebookServerInfo: ko.Observable; - public notebookWorkspaceManager: NotebookWorkspaceManager; - public sparkClusterConnectionInfo: ko.Observable; - public isSparkEnabled: ko.Observable; - public isSparkEnabledForAccount: ko.Observable; - public arcadiaToken: ko.Observable; - public arcadiaWorkspaces: ko.ObservableArray; - public hasStorageAnalyticsAfecFeature: ko.Observable; - public isSynapseLinkUpdating: ko.Observable; - public memoryUsageInfo: ko.Observable; - public notebookManager?: any; // This is dynamically loaded - - private _panes: ContextualPaneBase[] = []; - private _importExplorerConfigComplete: boolean = false; - private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = database => false; - private _isInitializingNotebooks: boolean; - private _isInitializingSparkConnectionInfo: boolean; - private notebookBasePath: ko.Observable; - private _arcadiaManager: ArcadiaResourceManager; - private notebookToImport: { - name: string; - content: string; - }; - - // React adapters - private commandBarComponentAdapter: CommandBarComponentAdapter; - private splashScreenAdapter: SplashScreenComponentAdapter; - private notificationConsoleComponentAdapter: NotificationConsoleComponentAdapter; - private dialogComponentAdapter: DialogComponentAdapter; - private _dialogProps: ko.Observable; - private addSynapseLinkDialog: DialogComponentAdapter; - private _addSynapseLinkDialogProps: ko.Observable; - - private static readonly MaxNbDatabasesToAutoExpand = 5; - - constructor() { - const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { - dataExplorerArea: Constants.Areas.ResourceTree - }); - this.addCollectionText = ko.observable("New Collection"); - this.addDatabaseText = ko.observable("New Database"); - this.hasWriteAccess = ko.observable(true); - this.collectionTitle = ko.observable("Collections"); - this.collectionTreeNodeAltText = ko.observable("Collection"); - this.deleteCollectionText = ko.observable("Delete Collection"); - this.deleteDatabaseText = ko.observable("Delete Database"); - this.refreshTreeTitle = ko.observable("Refresh collections"); - - this.databaseAccount = ko.observable(); - this.subscriptionType = ko.observable(SharedConstants.CollectionCreation.DefaultSubscriptionType); - let firstInitialization = true; - this.isRefreshingExplorer = ko.observable(true); - this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => { - if (!isRefreshing && firstInitialization) { - // set focus on first element - firstInitialization = false; - try { - document.getElementById("createNewContainerCommandButton").parentElement.parentElement.focus(); - } catch (e) { - Logger.logWarning( - "getElementById('createNewContainerCommandButton') failed to find element", - "Explorer/this.isRefreshingExplorer.subscribe" - ); - } - } - }); - this.isAccountReady = ko.observable(false); - this._isInitializingNotebooks = false; - this._isInitializingSparkConnectionInfo = false; - this.arcadiaToken = ko.observable(); - this.arcadiaToken.subscribe((token: string) => { - if (token) { - const notebookTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2); - (notebookTabs || []).forEach((tab: NotebookV2Tab) => { - tab.reconfigureServiceEndpoints(); - }); - } - }); - this.isNotebooksEnabledForAccount = ko.observable(false); - this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); - this.isSparkEnabledForAccount = ko.observable(false); - this.isSparkEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); - this.hasStorageAnalyticsAfecFeature = ko.observable(false); - this.hasStorageAnalyticsAfecFeature.subscribe((enabled: boolean) => this.refreshCommandBarButtons()); - this.isSynapseLinkUpdating = ko.observable(false); - this.isAccountReady.subscribe(async (isAccountReady: boolean) => { - if (isAccountReady) { - this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); - RouteHandler.getInstance().initHandler(); - this.notebookWorkspaceManager = new NotebookWorkspaceManager(); - this.arcadiaWorkspaces = ko.observableArray(); - this._arcadiaManager = new ArcadiaResourceManager(); - this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered => - this.hasStorageAnalyticsAfecFeature(isRegistered) - ); - Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then( - async () => { - this.isNotebookEnabled( - !this.isAuthWithResourceToken() && - ((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) || - this.isFeatureEnabled(Constants.Features.enableNotebooks)) - ); - - TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { - isNotebookEnabled: this.isNotebookEnabled(), - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook - }); - - if (this.isNotebookEnabled()) { - await this.initNotebooks(this.databaseAccount()); - const workspaces = await this._getArcadiaWorkspaces(); - this.arcadiaWorkspaces(workspaces); - } else if (this.notebookToImport) { - // if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane - this._openSetupNotebooksPaneForQuickstart(); - } - - this.isSparkEnabled( - (this.isNotebookEnabled() && - this.isSparkEnabledForAccount() && - this.arcadiaWorkspaces() && - this.arcadiaWorkspaces().length > 0) || - this.isFeatureEnabled(Constants.Features.enableSpark) - ); - if (this.isSparkEnabled()) { - const pollArcadiaTokenRefresh = async () => { - this.arcadiaToken(await this.getArcadiaToken()); - setTimeout(() => pollArcadiaTokenRefresh(), this.getTokenRefreshInterval(this.arcadiaToken())); - }; - await pollArcadiaTokenRefresh(); - } - } - ); - } - }); - this.memoryUsageInfo = ko.observable(); - - this.features = ko.observable(); - this.serverId = ko.observable(); - this.queriesClient = new QueriesClient(this); - this.isTryCosmosDBSubscription = ko.observable(false); - - this.resourceTokenDatabaseId = ko.observable(); - this.resourceTokenCollectionId = ko.observable(); - this.resourceTokenCollection = ko.observable(); - this.resourceTokenPartitionKey = ko.observable(); - this.isAuthWithResourceToken = ko.observable(false); - - this.shareAccessData = ko.observable({ - readWriteUrl: undefined, - readUrl: undefined - }); - this.tokenForRenewal = ko.observable(""); - this.renewTokenError = ko.observable(""); - this.shareAccessUrl = ko.observable(); - this.shareUrlCopyHelperText = ko.observable("Click to copy"); - this.shareTokenCopyHelperText = ko.observable("Click to copy"); - this.shareAccessToggleState = ko.observable(ShareAccessToggleState.ReadWrite); - this.shareAccessToggleState.subscribe((toggleState: ShareAccessToggleState) => { - if (toggleState === ShareAccessToggleState.ReadWrite) { - this.shareAccessUrl(this.shareAccessData && this.shareAccessData().readWriteUrl); - } else { - this.shareAccessUrl(this.shareAccessData && this.shareAccessData().readUrl); - } - }); - this.shouldShowShareDialogContents = ko.observable(false); - this.shouldShowDataAccessExpiryDialog = ko.observable(false); - this.shouldShowContextSwitchPrompt = ko.observable(false); - this.isGalleryPublishEnabled = ko.computed(() => - this.isFeatureEnabled(Constants.Features.enableGalleryPublish) - ); - this.isLinkInjectionEnabled = ko.computed(() => - this.isFeatureEnabled(Constants.Features.enableLinkInjection) - ); - this.isGitHubPaneEnabled = ko.observable(false); - this.isMongoIndexingEnabled = ko.observable(false); - this.isPublishNotebookPaneEnabled = ko.observable(false); - this.isCopyNotebookPaneEnabled = ko.observable(false); - - this.canExceedMaximumValue = ko.computed(() => - this.isFeatureEnabled(Constants.Features.canExceedMaximumValue) - ); - - this.isSchemaEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableSchema)); - this.isNotificationConsoleExpanded = ko.observable(false); - - this.databases = ko.observableArray(); - this.canSaveQueries = ko.computed(() => { - const savedQueriesDatabase: ViewModels.Database = _.find( - this.databases(), - (database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName - ); - if (!savedQueriesDatabase) { - return false; - } - const savedQueriesCollection: ViewModels.Collection = - savedQueriesDatabase && - _.find( - savedQueriesDatabase.collections(), - (collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName - ); - if (!savedQueriesCollection) { - return false; - } - return true; - }); - this.isLeftPaneExpanded = ko.observable(true); - this.selectedNode = ko.observable(); - this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { - // Make sure switching tabs restores tabs display - this.isTabsContentExpanded(false); - }); - this.isResourceTokenCollectionNodeSelected = ko.computed(() => { - return ( - this.selectedNode() && - this.resourceTokenCollection() && - this.selectedNode().id() === this.resourceTokenCollection().id() - ); - }); - - const splitterBounds: SplitterBounds = { - min: ExplorerMetrics.SplitterMinWidth, - max: ExplorerMetrics.SplitterMaxWidth - }; - this.splitter = new Splitter({ - splitterId: "h_splitter1", - leftId: "resourcetree", - bounds: splitterBounds, - direction: SplitterDirection.Vertical - }); - this.notificationConsoleData = ko.observableArray([]); - this.defaultExperience = ko.observable(); - this.databaseAccount.subscribe(databaseAccount => { - const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount( - databaseAccount - ); - this.defaultExperience(defaultExperience); - updateUserContext({ - defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience) - }); - }); - - this.isPreferredApiDocumentDB = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.DocumentDB.toLowerCase(); - }); - - this.isPreferredApiCassandra = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase(); - }); - this.isPreferredApiGraph = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Graph.toLowerCase(); - }); - - this.isPreferredApiTable = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Table.toLowerCase(); - }); - - this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => { - if (this.isFeatureEnabled(Constants.Features.enableFixedCollectionWithSharedThroughput)) { - return true; - } - - if (this.databaseAccount && !this.databaseAccount()) { - return false; - } - - return this.isEnableMongoCapabilityPresent(); - }); - - this.isServerlessEnabled = ko.computed( - () => - this.databaseAccount && - this.databaseAccount()?.properties?.capabilities?.find( - item => item.name === Constants.CapabilityNames.EnableServerless - ) !== undefined - ); - - this.isPreferredApiMongoDB = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) { - return true; - } - - if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase()) { - return true; - } - - if ( - this.databaseAccount && - this.databaseAccount() && - this.databaseAccount().kind.toLowerCase() === Constants.AccountKind.MongoDB - ) { - return true; - } - - return false; - }); - - this.isEnableMongoCapabilityPresent = ko.computed(() => { - const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities; - if (!capabilities) { - return false; - } - - for (let i = 0; i < capabilities.length; i++) { - if (typeof capabilities[i] === "object" && capabilities[i].name === Constants.CapabilityNames.EnableMongo) { - return true; - } - } - - return false; - }); - - this.isHostedDataExplorerEnabled = ko.computed( - () => - configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph() - ); - this.isRightPanelV2Enabled = ko.computed(() => - this.isFeatureEnabled(Constants.Features.enableRightPanelV2) - ); - this.defaultExperience.subscribe((defaultExperience: string) => { - if ( - defaultExperience && - defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase() - ) { - this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => { - return database.id() === "system"; - }; - } - }); - - this.selectedDatabaseId = ko.computed(() => { - const selectedNode = this.selectedNode(); - if (!selectedNode) { - return ""; - } - - switch (selectedNode.nodeKind) { - case "Collection": - return (selectedNode as ViewModels.CollectionBase).databaseId || ""; - case "Database": - return selectedNode.id() || ""; - case "DocumentId": - case "StoredProcedure": - case "Trigger": - case "UserDefinedFunction": - return selectedNode.collection.databaseId || ""; - default: - return ""; - } - }); - - this.nonSystemDatabases = ko.computed(() => { - return this.databases().filter((database: ViewModels.Database) => !this._isSystemDatabasePredicate(database)); - }); - - this.addDatabasePane = new AddDatabasePane({ - id: "adddatabasepane", - visible: ko.observable(false), - - container: this - }); - - this.addCollectionPane = new AddCollectionPane({ - isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()), - id: "addcollectionpane", - visible: ko.observable(false), - - container: this - }); - - this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({ - id: "deletecollectionconfirmationpane", - visible: ko.observable(false), - - container: this - }); - - this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({ - id: "deletedatabaseconfirmationpane", - visible: ko.observable(false), - - container: this - }); - - this.graphStylingPane = new GraphStylingPane({ - id: "graphstylingpane", - visible: ko.observable(false), - - container: this - }); - - this.addTableEntityPane = new AddTableEntityPane({ - id: "addtableentitypane", - visible: ko.observable(false), - - container: this - }); - - this.editTableEntityPane = new EditTableEntityPane({ - id: "edittableentitypane", - visible: ko.observable(false), - - container: this - }); - - this.tableColumnOptionsPane = new TableColumnOptionsPane({ - id: "tablecolumnoptionspane", - visible: ko.observable(false), - - container: this - }); - - this.querySelectPane = new QuerySelectPane({ - id: "queryselectpane", - visible: ko.observable(false), - - container: this - }); - - this.newVertexPane = new NewVertexPane({ - id: "newvertexpane", - visible: ko.observable(false), - - container: this - }); - - this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ - id: "cassandraaddcollectionpane", - visible: ko.observable(false), - - container: this - }); - - this.settingsPane = new SettingsPane({ - id: "settingspane", - visible: ko.observable(false), - - container: this - }); - - this.executeSprocParamsPane = new ExecuteSprocParamsPane({ - id: "executesprocparamspane", - visible: ko.observable(false), - - container: this - }); - - this.renewAdHocAccessPane = new RenewAdHocAccessPane({ - id: "renewadhocaccesspane", - visible: ko.observable(false), - - container: this - }); - - this.uploadItemsPane = new UploadItemsPane({ - id: "uploaditemspane", - visible: ko.observable(false), - - container: this - }); - - this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this); - - this.loadQueryPane = new LoadQueryPane({ - id: "loadquerypane", - visible: ko.observable(false), - - container: this - }); - - this.saveQueryPane = new SaveQueryPane({ - id: "savequerypane", - visible: ko.observable(false), - - container: this - }); - - this.browseQueriesPane = new BrowseQueriesPane({ - id: "browsequeriespane", - visible: ko.observable(false), - - container: this - }); - - this.uploadFilePane = new UploadFilePane({ - id: "uploadfilepane", - visible: ko.observable(false), - - container: this - }); - - this.stringInputPane = new StringInputPane({ - id: "stringinputpane", - visible: ko.observable(false), - - container: this - }); - - this.setupNotebooksPane = new SetupNotebooksPane({ - id: "setupnotebookspane", - visible: ko.observable(false), - - container: this - }); - - this.tabsManager = new TabsManager(); - - this._panes = [ - this.addDatabasePane, - this.addCollectionPane, - this.deleteCollectionConfirmationPane, - this.deleteDatabaseConfirmationPane, - this.graphStylingPane, - this.addTableEntityPane, - this.editTableEntityPane, - this.tableColumnOptionsPane, - this.querySelectPane, - this.newVertexPane, - this.cassandraAddCollectionPane, - this.settingsPane, - this.executeSprocParamsPane, - this.renewAdHocAccessPane, - this.uploadItemsPane, - this.loadQueryPane, - this.saveQueryPane, - this.browseQueriesPane, - this.uploadFilePane, - this.stringInputPane, - this.setupNotebooksPane - ]; - this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); - this.isTabsContentExpanded = ko.observable(false); - - document.addEventListener( - "contextmenu", - function(e) { - e.preventDefault(); - }, - false - ); - - $(function() { - $(document.body).click(() => $(".commandDropdownContainer").hide()); - }); - - // TODO move this to API customization class - this.defaultExperience.subscribe(defaultExperience => { - const defaultExperienceNormalizedString = ( - defaultExperience || Constants.DefaultAccountExperience.Default - ).toLowerCase(); - - switch (defaultExperienceNormalizedString) { - case Constants.DefaultAccountExperience.DocumentDB.toLowerCase(): - this.addCollectionText("New Container"); - this.addDatabaseText("New Database"); - this.collectionTitle("SQL API"); - this.collectionTreeNodeAltText("Container"); - this.deleteCollectionText("Delete Container"); - this.deleteDatabaseText("Delete Database"); - this.addCollectionPane.title("Add Container"); - this.addCollectionPane.collectionIdTitle("Container id"); - this.addCollectionPane.collectionWithThroughputInSharedTitle( - "Provision dedicated throughput for this container" - ); - this.deleteCollectionConfirmationPane.title("Delete Container"); - this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id"); - this.refreshTreeTitle("Refresh containers"); - break; - case Constants.DefaultAccountExperience.MongoDB.toLowerCase(): - case Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase(): - this.addCollectionText("New Collection"); - this.addDatabaseText("New Database"); - this.collectionTitle("Collections"); - this.collectionTreeNodeAltText("Collection"); - this.deleteCollectionText("Delete Collection"); - this.deleteDatabaseText("Delete Database"); - this.addCollectionPane.title("Add Collection"); - this.addCollectionPane.collectionIdTitle("Collection id"); - this.addCollectionPane.collectionWithThroughputInSharedTitle( - "Provision dedicated throughput for this collection" - ); - this.refreshTreeTitle("Refresh collections"); - break; - case Constants.DefaultAccountExperience.Graph.toLowerCase(): - this.addCollectionText("New Graph"); - this.addDatabaseText("New Database"); - this.deleteCollectionText("Delete Graph"); - this.deleteDatabaseText("Delete Database"); - this.collectionTitle("Gremlin API"); - this.collectionTreeNodeAltText("Graph"); - this.addCollectionPane.title("Add Graph"); - this.addCollectionPane.collectionIdTitle("Graph id"); - this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph"); - this.deleteCollectionConfirmationPane.title("Delete Graph"); - this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id"); - this.refreshTreeTitle("Refresh graphs"); - break; - case Constants.DefaultAccountExperience.Table.toLowerCase(): - this.addCollectionText("New Table"); - this.addDatabaseText("New Database"); - this.deleteCollectionText("Delete Table"); - this.deleteDatabaseText("Delete Database"); - this.collectionTitle("Azure Table API"); - this.collectionTreeNodeAltText("Table"); - this.addCollectionPane.title("Add Table"); - this.addCollectionPane.collectionIdTitle("Table id"); - this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); - this.refreshTreeTitle("Refresh tables"); - this.addTableEntityPane.title("Add Table Entity"); - this.editTableEntityPane.title("Edit Table Entity"); - this.deleteCollectionConfirmationPane.title("Delete Table"); - this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); - this.tableDataClient = new TablesAPIDataClient(); - break; - case Constants.DefaultAccountExperience.Cassandra.toLowerCase(): - this.addCollectionText("New Table"); - this.addDatabaseText("New Keyspace"); - this.deleteCollectionText("Delete Table"); - this.deleteDatabaseText("Delete Keyspace"); - this.collectionTitle("Cassandra API"); - this.collectionTreeNodeAltText("Table"); - this.addCollectionPane.title("Add Table"); - this.addCollectionPane.collectionIdTitle("Table id"); - this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); - this.refreshTreeTitle("Refresh tables"); - this.addTableEntityPane.title("Add Table Row"); - this.editTableEntityPane.title("Edit Table Row"); - this.deleteCollectionConfirmationPane.title("Delete Table"); - this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); - this.deleteDatabaseConfirmationPane.title("Delete Keyspace"); - this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id"); - this.tableDataClient = new CassandraAPIDataClient(); - break; - } - }); - - this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); - this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this); - - this._initSettings(); - - TelemetryProcessor.traceSuccess( - Action.InitializeDataExplorer, - { dataExplorerArea: Constants.Areas.ResourceTree }, - startKey - ); - - this.isNotebookEnabled = ko.observable(false); - this.isNotebookEnabled.subscribe(async () => { - if (!this.notebookManager) { - const notebookManagerModule = await import( - /* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager" - ); - this.notebookManager = new notebookManagerModule.default(); - this.notebookManager.initialize({ - container: this, - dialogProps: this._dialogProps, - notebookBasePath: this.notebookBasePath, - resourceTree: this.resourceTree, - refreshCommandBarButtons: () => this.refreshCommandBarButtons(), - refreshNotebookList: () => this.refreshNotebookList() - }); - - this.gitHubReposPane = this.notebookManager.gitHubReposPane; - this.isGitHubPaneEnabled(true); - } - - this.refreshCommandBarButtons(); - this.refreshNotebookList(); - }); - - this.isSparkEnabled = ko.observable(false); - this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons()); - this.resourceTree = new ResourceTreeAdapter(this); - this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this); - this.notebookServerInfo = ko.observable({ - notebookServerEndpoint: undefined, - authToken: undefined - }); - this.notebookBasePath = ko.observable(Constants.Notebook.defaultBasePath); - this.sparkClusterConnectionInfo = ko.observable({ - userName: undefined, - password: undefined, - endpoints: [] - }); - - // Override notebook server parameters from URL parameters - const featureSubcription = this.features.subscribe(features => { - const serverInfo = this.notebookServerInfo(); - if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) { - serverInfo.notebookServerEndpoint = features[Constants.Features.notebookServerUrl]; - } - - if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) { - serverInfo.authToken = features[Constants.Features.notebookServerToken]; - } - this.notebookServerInfo(serverInfo); - this.notebookServerInfo.valueHasMutated(); - - if (this.isFeatureEnabled(Constants.Features.notebookBasePath)) { - this.notebookBasePath(features[Constants.Features.notebookBasePath]); - } - - if (this.isFeatureEnabled(Constants.Features.livyEndpoint)) { - this.sparkClusterConnectionInfo({ - userName: undefined, - password: undefined, - endpoints: [ - { - endpoint: features[Constants.Features.livyEndpoint], - kind: DataModels.SparkClusterEndpointKind.Livy - } - ] - }); - this.sparkClusterConnectionInfo.valueHasMutated(); - } - - if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) { - updateUserContext({ useSDKOperations: true }); - } - - featureSubcription.dispose(); - }); - - this._dialogProps = ko.observable({ - isModal: false, - visible: false, - title: undefined, - subText: undefined, - primaryButtonText: undefined, - secondaryButtonText: undefined, - onPrimaryButtonClick: undefined, - onSecondaryButtonClick: undefined - }); - this.dialogComponentAdapter = new DialogComponentAdapter(); - this.dialogComponentAdapter.parameters = this._dialogProps; - this.splashScreenAdapter = new SplashScreenComponentAdapter(this); - this.mostRecentActivity = new MostRecentActivity.MostRecentActivity(this); - - this._addSynapseLinkDialogProps = ko.observable({ - isModal: false, - visible: false, - title: undefined, - subText: undefined, - primaryButtonText: undefined, - secondaryButtonText: undefined, - onPrimaryButtonClick: undefined, - onSecondaryButtonClick: undefined - }); - this.addSynapseLinkDialog = new DialogComponentAdapter(); - this.addSynapseLinkDialog.parameters = this._addSynapseLinkDialogProps; - } - - public openEnableSynapseLinkDialog(): void { - const addSynapseLinkDialogProps: DialogProps = { - linkProps: { - linkText: "Learn more", - linkUrl: "https://aka.ms/cosmosdb-synapselink" - }, - isModal: true, - visible: true, - title: `Enable Azure Synapse Link on your Cosmos DB account`, - subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads. - Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`, - primaryButtonText: "Enable Azure Synapse Link", - secondaryButtonText: "Cancel", - - onPrimaryButtonClick: async () => { - const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); - const logId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - "Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account." - ); - this.isSynapseLinkUpdating(true); - this._closeSynapseLinkModalDialog(); - - const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id); - - try { - const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync( - this.databaseAccount().id, - "2019-12-12", - { - properties: { - enableAnalyticalStorage: true - } - } - ); - NotificationConsoleUtils.clearInProgressMessageWithId(logId); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Info, - "Enabled Azure Synapse Link for this account" - ); - TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, startTime); - this.databaseAccount(databaseAccount); - } catch (error) { - NotificationConsoleUtils.clearInProgressMessageWithId(logId); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}` - ); - TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, startTime); - } finally { - this.isSynapseLinkUpdating(false); - } - }, - - onSecondaryButtonClick: () => { - this._closeSynapseLinkModalDialog(); - TelemetryProcessor.traceCancel(Action.EnableAzureSynapseLink); - } - }; - this._addSynapseLinkDialogProps(addSynapseLinkDialogProps); - TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); - - // TODO: return result - } - - public copyUrlLink(src: any, event: MouseEvent): void { - const urlLinkInput: HTMLInputElement = document.getElementById("shareUrlLink") as HTMLInputElement; - urlLinkInput && urlLinkInput.select(); - document.execCommand("copy"); - this.shareUrlCopyHelperText("Copied"); - setTimeout(() => this.shareUrlCopyHelperText("Click to copy"), Constants.ClientDefaults.copyHelperTimeoutMs); - - TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - description: "Copy full screen URL", - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ShareDialog - }); - } - - public onCopyUrlLinkKeyPress(src: any, event: KeyboardEvent): boolean { - if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { - this.copyUrlLink(src, null); - return false; - } - - return true; - } - - public copyToken(src: any, event: MouseEvent): void { - const tokenInput: HTMLInputElement = document.getElementById("shareToken") as HTMLInputElement; - tokenInput && tokenInput.select(); - document.execCommand("copy"); - this.shareTokenCopyHelperText("Copied"); - setTimeout(() => this.shareTokenCopyHelperText("Click to copy"), Constants.ClientDefaults.copyHelperTimeoutMs); - } - - public onCopyTokenKeyPress(src: any, event: KeyboardEvent): boolean { - if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { - this.copyToken(src, null); - return false; - } - - return true; - } - - public renewToken = (): void => { - TelemetryProcessor.trace(Action.ConnectEncryptionToken); - this.renewTokenError(""); - const id: string = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - "Initiating connection to account" - ); - this.renewExplorerShareAccess(this, this.tokenForRenewal()) - .fail((error: any) => { - const stringifiedError: string = getErrorMessage(error); - this.renewTokenError("Invalid connection string specified"); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to initiate connection to account: ${stringifiedError}` - ); - }) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); - }; - - public generateSharedAccessData(): void { - const id: string = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Generating share url"); - AuthHeadersUtil.generateEncryptedToken().then( - (tokenResponse: DataModels.GenerateTokenResponse) => { - NotificationConsoleUtils.clearInProgressMessageWithId(id); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully generated share url"); - this.shareAccessData({ - readWriteUrl: this._getShareAccessUrlForToken(tokenResponse.readWrite), - readUrl: this._getShareAccessUrlForToken(tokenResponse.read) - }); - !this.shareAccessData().readWriteUrl && this.shareAccessToggleState(ShareAccessToggleState.Read); // select read toggle by default for readers - this.shareAccessToggleState.valueHasMutated(); // to set initial url and token state - this.shareAccessData.valueHasMutated(); - this._openShareDialog(); - }, - (error: any) => { - NotificationConsoleUtils.clearInProgressMessageWithId(id); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to generate share url: ${getErrorMessage(error)}` - ); - console.error(error); - } - ); - } - - public renewShareAccess(token: string): Q.Promise { - if (!this.renewExplorerShareAccess) { - return Q.reject("Not implemented"); - } - - const deferred: Q.Deferred = Q.defer(); - const id: string = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - "Initiating connection to account" - ); - this.renewExplorerShareAccess(this, token) - .then( - () => { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Connection successful"); - this.renewAdHocAccessPane && this.renewAdHocAccessPane.close(); - deferred.resolve(); - }, - (error: any) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to connect: ${getErrorMessage(error)}` - ); - deferred.reject(error); - } - ) - .finally(() => { - NotificationConsoleUtils.clearInProgressMessageWithId(id); - }); - - return deferred.promise; - } - - public displayGuestAccessTokenRenewalPrompt(): void { - if (!$("#dataAccessTokenModal").dialog("instance")) { - const connectButton = { - text: "Connect", - class: "connectDialogButtons connectButton connectOkBtns", - click: () => { - this.renewAdHocAccessPane.open(); - $("#dataAccessTokenModal").dialog("close"); - } - }; - const cancelButton = { - text: "Cancel", - class: "connectDialogButtons cancelBtn", - click: () => { - $("#dataAccessTokenModal").dialog("close"); - } - }; - - $("#dataAccessTokenModal").dialog({ - autoOpen: false, - buttons: [connectButton, cancelButton], - closeOnEscape: false, - draggable: false, - dialogClass: "no-close", - height: 180, - modal: true, - position: { my: "center center", at: "center center", of: window }, - resizable: false, - title: "Temporary access expired", - width: 435, - close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowDataAccessExpiryDialog(false) - }); - $("#dataAccessTokenModal").dialog("option", "classes", { - "ui-dialog-titlebar": "connectTitlebar" - }); - } - this.shouldShowDataAccessExpiryDialog(true); - $("#dataAccessTokenModal").dialog("open"); - } - - public isConnectExplorerVisible(): boolean { - return $("#connectExplorer").is(":visible") || false; - } - - public displayContextSwitchPromptForConnectionString(connectionString: string): void { - const yesButton = { - text: "OK", - class: "connectDialogButtons okBtn connectOkBtns", - click: () => { - $("#contextSwitchPrompt").dialog("close"); - this.tabsManager.closeTabs(); // clear all tabs so we dont leave any tabs from previous session open - this.renewShareAccess(connectionString); - } - }; - const noButton = { - text: "Cancel", - class: "connectDialogButtons cancelBtn", - click: () => { - $("#contextSwitchPrompt").dialog("close"); - } - }; - - if (!$("#contextSwitchPrompt").dialog("instance")) { - $("#contextSwitchPrompt").dialog({ - autoOpen: false, - buttons: [yesButton, noButton], - closeOnEscape: false, - draggable: false, - dialogClass: "no-close", - height: 255, - modal: true, - position: { my: "center center", at: "center center", of: window }, - resizable: false, - title: "Switch account", - width: 440, - close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowDataAccessExpiryDialog(false) - }); - $("#contextSwitchPrompt").dialog("option", "classes", { - "ui-dialog-titlebar": "connectTitlebar" - }); - $("#contextSwitchPrompt").dialog("option", "open", (event: Event, ui: JQueryUI.DialogUIParams) => { - $(".ui-dialog ").css("z-index", 1001); - $("#contextSwitchPrompt") - .parent() - .siblings(".ui-widget-overlay") - .css("z-index", 1000); - }); - } - $("#contextSwitchPrompt").dialog("option", "buttons", [yesButton, noButton]); // rebind buttons so callbacks accept current connection string - this.shouldShowContextSwitchPrompt(true); - $("#contextSwitchPrompt").dialog("open"); - } - - public displayConnectExplorerForm(): void { - $("#divExplorer").hide(); - $("#connectExplorer").css("display", "flex"); - } - - public hideConnectExplorerForm(): void { - $("#connectExplorer").hide(); - $("#divExplorer").show(); - } - - public isReadWriteToggled: () => boolean = (): boolean => { - return this.shareAccessToggleState() === ShareAccessToggleState.ReadWrite; - }; - - public isReadToggled: () => boolean = (): boolean => { - return this.shareAccessToggleState() === ShareAccessToggleState.Read; - }; - - public toggleReadWrite: (src: any, event: MouseEvent) => void = (src: any, event: MouseEvent) => { - this.shareAccessToggleState(ShareAccessToggleState.ReadWrite); - }; - - public toggleRead: (src: any, event: MouseEvent) => void = (src: any, event: MouseEvent) => { - this.shareAccessToggleState(ShareAccessToggleState.Read); - }; - - public onToggleKeyDown: (src: any, event: KeyboardEvent) => boolean = (src: any, event: KeyboardEvent) => { - if (event.keyCode === Constants.KeyCodes.LeftArrow) { - this.toggleReadWrite(src, null); - return false; - } else if (event.keyCode === Constants.KeyCodes.RightArrow) { - this.toggleRead(src, null); - return false; - } - return true; - }; - - public isDatabaseNodeOrNoneSelected(): boolean { - return this.isNoneSelected() || this.isDatabaseNodeSelected(); - } - - public isDatabaseNodeSelected(): boolean { - return (this.selectedNode() && this.selectedNode().nodeKind === "Database") || false; - } - - public isNodeKindSelected(nodeKind: string): boolean { - return (this.selectedNode() && this.selectedNode().nodeKind === nodeKind) || false; - } - - public isNoneSelected(): boolean { - return this.selectedNode() == null; - } - - public isFeatureEnabled(feature: string): boolean { - const features = this.features(); - - if (!features) { - return false; - } - - if (feature in features && features[feature]) { - return true; - } - - return false; - } - - public logConsoleData(consoleData: ConsoleData): void { - this.notificationConsoleData.splice(0, 0, consoleData); - } - - public deleteInProgressConsoleDataWithId(id: string): void { - const updatedConsoleData = _.reject( - this.notificationConsoleData(), - (data: ConsoleData) => data.type === ConsoleDataType.InProgress && data.id === id - ); - this.notificationConsoleData(updatedConsoleData); - } - - public expandConsole(): void { - this.isNotificationConsoleExpanded(true); - } - - public collapseConsole(): void { - this.isNotificationConsoleExpanded(false); - } - - public toggleLeftPaneExpanded() { - this.isLeftPaneExpanded(!this.isLeftPaneExpanded()); - - if (this.isLeftPaneExpanded()) { - document.getElementById("expandToggleLeftPaneButton").focus(); - this.splitter.expandLeft(); - } else { - document.getElementById("collapseToggleLeftPaneButton").focus(); - this.splitter.collapseLeft(); - } - } - - public refreshDatabaseForResourceToken(): Q.Promise { - const databaseId = this.resourceTokenDatabaseId(); - const collectionId = this.resourceTokenCollectionId(); - if (!databaseId || !collectionId) { - return Q.reject(); - } - - const deferred: Q.Deferred = Q.defer(); - readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { - this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection)); - this.selectedNode(this.resourceTokenCollection()); - deferred.resolve(); - }); - - return deferred.promise; - } - - public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise { - this.isRefreshingExplorer(true); - const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - let resourceTreeStartKey: number = null; - if (isInitialLoad) { - resourceTreeStartKey = TelemetryProcessor.traceStart(Action.LoadResourceTree, { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - } - - // TODO: Refactor - const deferred: Q.Deferred = Q.defer(); - this._setLoadingStatusText("Fetching databases..."); - readDatabases().then( - (databases: DataModels.Database[]) => { - this._setLoadingStatusText("Successfully fetched databases."); - TelemetryProcessor.traceSuccess( - Action.LoadDatabases, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }, - startKey - ); - const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode(); - const deltaDatabases = this.getDeltaDatabases(databases); - this.addDatabasesToList(deltaDatabases.toAdd); - this.deleteDatabasesFromList(deltaDatabases.toDelete); - this.selectedNode(currentlySelectedNode); - this._setLoadingStatusText("Fetching containers..."); - this.refreshAndExpandNewDatabases(deltaDatabases.toAdd) - .then( - () => { - this._setLoadingStatusText("Successfully fetched containers."); - deferred.resolve(); - }, - reason => { - this._setLoadingStatusText("Failed to fetch containers."); - deferred.reject(reason); - } - ) - .finally(() => this.isRefreshingExplorer(false)); - }, - error => { - this._setLoadingStatusText("Failed to fetch databases."); - this.isRefreshingExplorer(false); - deferred.reject(error); - const errorMessage = getErrorMessage(error); - TelemetryProcessor.traceFailure( - Action.LoadDatabases, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree, - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while refreshing databases: ${errorMessage}` - ); - } - ); - - return deferred.promise.then( - () => { - if (resourceTreeStartKey != null) { - TelemetryProcessor.traceSuccess( - Action.LoadResourceTree, - { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }, - resourceTreeStartKey - ); - } - }, - error => { - if (resourceTreeStartKey != null) { - TelemetryProcessor.traceFailure( - Action.LoadResourceTree, - { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree, - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }, - resourceTreeStartKey - ); - } - } - ); - } - - public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.onRefreshResourcesClick(source, null); - return false; - } - return true; - }; - - public onRefreshResourcesClick = (source: any, event: MouseEvent): void => { - const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { - description: "Refresh button clicked", - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - this.isRefreshingExplorer(true); - this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); - this.refreshNotebookList(); - }; - - public toggleLeftPaneExpandedKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.toggleLeftPaneExpanded(); - return false; - } - return true; - }; - - // Facade - public provideFeedbackEmail = () => { - window.open(Constants.Urls.feedbackEmail, "_self"); - }; - - public async getArcadiaToken(): Promise { - return new Promise((resolve: (token: string) => void, reject: (error: any) => void) => { - sendCachedDataMessage(MessageTypes.GetArcadiaToken, undefined /** params **/).then( - (token: string) => { - resolve(token); - }, - (error: any) => { - Logger.logError(getErrorMessage(error), "Explorer/getArcadiaToken"); - resolve(undefined); - } - ); - }); - } - - private async _getArcadiaWorkspaces(): Promise { - try { - const workspaces = await this._arcadiaManager.listWorkspacesAsync([userContext.subscriptionId]); - let workspaceItems: ArcadiaWorkspaceItem[] = new Array(workspaces.length); - const sparkPromises: Promise[] = []; - workspaces.forEach((workspace, i) => { - let promise = this._arcadiaManager.listSparkPoolsAsync(workspaces[i].id).then( - sparkpools => { - workspaceItems[i] = { ...workspace, sparkPools: sparkpools }; - }, - error => { - Logger.logError(getErrorMessage(error), "Explorer/this._arcadiaManager.listSparkPoolsAsync"); - } - ); - sparkPromises.push(promise); - }); - - return Promise.all(sparkPromises).then(() => workspaceItems); - } catch (error) { - handleError(error, "Explorer/this._arcadiaManager.listWorkspacesAsync", "Get Arcadia workspaces failed"); - return Promise.resolve([]); - } - } - - public async createWorkspace(): Promise { - return sendCachedDataMessage(MessageTypes.CreateWorkspace, undefined /** params **/); - } - - public async createSparkPool(workspaceId: string): Promise { - return sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]); - } - - public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - throw new Error("No database account specified"); - } - - if (this._isInitializingNotebooks) { - return; - } - this._isInitializingNotebooks = true; - - await this.ensureNotebookWorkspaceRunning(); - let connectionInfo: DataModels.NotebookWorkspaceConnectionInfo = { - authToken: undefined, - notebookServerEndpoint: undefined - }; - try { - connectionInfo = await this.notebookWorkspaceManager.getNotebookConnectionInfoAsync( - databaseAccount.id, - "default" - ); - } catch (error) { - this._isInitializingNotebooks = false; - handleError( - error, - "initNotebooks/getNotebookConnectionInfoAsync", - `Failed to get notebook workspace connection info: ${getErrorMessage(error)}` - ); - throw error; - } finally { - // Overwrite with feature flags - if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) { - connectionInfo.notebookServerEndpoint = this.features()[Constants.Features.notebookServerUrl]; - } - - if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) { - connectionInfo.authToken = this.features()[Constants.Features.notebookServerToken]; - } - - this.notebookServerInfo(connectionInfo); - this.notebookServerInfo.valueHasMutated(); - this.refreshNotebookList(); - } - - this._isInitializingNotebooks = false; - } - - public resetNotebookWorkspace() { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) { - handleError( - "Attempt to reset notebook workspace, but notebook is not enabled", - "Explorer/resetNotebookWorkspace" - ); - return; - } - const resetConfirmationDialogProps: DialogProps = { - isModal: true, - visible: true, - title: "Reset Workspace", - subText: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?", - primaryButtonText: "OK", - secondaryButtonText: "Cancel", - onPrimaryButtonClick: this._resetNotebookWorkspace, - onSecondaryButtonClick: this._closeModalDialog - }; - this._dialogProps(resetConfirmationDialogProps); - } - - private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { - if (!databaseAccount) { - return false; - } - - try { - const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id); - return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default"); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); - return false; - } - } - - private async ensureNotebookWorkspaceRunning() { - if (!this.databaseAccount()) { - return; - } - - let clearMessage; - try { - const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync( - this.databaseAccount().id, - "default" - ); - if ( - notebookWorkspace && - notebookWorkspace.properties && - notebookWorkspace.properties.status && - notebookWorkspace.properties.status.toLowerCase() === "stopped" - ) { - clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace"); - await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default"); - } - } catch (error) { - handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace"); - } finally { - clearMessage && clearMessage(); - } - } - - private _resetNotebookWorkspace = async () => { - this._closeModalDialog(); - const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace"); - try { - await this.notebookManager?.notebookClient.resetWorkspace(); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace"); - TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace); - } catch (error) { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`); - TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, { - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }); - throw error; - } finally { - NotificationConsoleUtils.clearInProgressMessageWithId(id); - } - }; - - private _closeModalDialog = () => { - this._dialogProps().visible = false; - this._dialogProps.valueHasMutated(); - }; - - private _closeSynapseLinkModalDialog = () => { - this._addSynapseLinkDialogProps().visible = false; - this._addSynapseLinkDialogProps.valueHasMutated(); - }; - - private _shouldProcessMessage(event: MessageEvent): boolean { - if (typeof event.data !== "object") { - return false; - } - if (event.data["signature"] !== "pcIframe") { - return false; - } - if (!("data" in event.data)) { - return false; - } - if (typeof event.data["data"] !== "object") { - return false; - } - - // before initialization completed give exception - const message = event.data.data; - if (!this._importExplorerConfigComplete && message && message.type) { - const messageType = message.type; - switch (messageType) { - case MessageTypes.SendNotification: - case MessageTypes.ClearNotification: - case MessageTypes.LoadingStatus: - case MessageTypes.InitTestExplorer: - return true; - } - } - if (!("inputs" in event.data["data"]) && !this._importExplorerConfigComplete) { - return false; - } - return true; - } - - public handleMessage(event: MessageEvent) { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - if (!this._shouldProcessMessage(event)) { - return; - } - - const message: any = event.data.data; - const inputs: ViewModels.DataExplorerInputsFrame = message.inputs; - - const isRunningInPortal = configContext.platform === Platform.Portal; - const isRunningInDevMode = process.env.NODE_ENV === "development"; - if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) { - inputs.extensionEndpoint = configContext.PROXY_PATH; - } - - this.initDataExplorerWithFrameInputs(inputs); - - const openAction: ActionContracts.DataExplorerAction = message.openAction; - if (!!openAction) { - if (this.isRefreshingExplorer()) { - const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => { - handleOpenAction(openAction, this.nonSystemDatabases(), this); - subscription.dispose(); - }); - } else { - handleOpenAction(openAction, this.nonSystemDatabases(), this); - } - } - if (message.actionType === ActionContracts.ActionType.TransmitCachedData) { - handleCachedDataMessage(message); - return; - } - if (message.type) { - switch (message.type) { - case MessageTypes.UpdateLocationHash: - if (!message.locationHash) { - break; - } - hasher.replaceHash(message.locationHash); - RouteHandler.getInstance().parseHash(message.locationHash); - break; - case MessageTypes.SendNotification: - if (!message.message) { - break; - } - NotificationConsoleUtils.logConsoleMessage( - message.consoleDataType || ConsoleDataType.Info, - message.message, - message.id - ); - break; - case MessageTypes.ClearNotification: - if (!message.id) { - break; - } - NotificationConsoleUtils.clearInProgressMessageWithId(message.id); - break; - case MessageTypes.LoadingStatus: - if (!message.text) { - break; - } - this._setLoadingStatusText(message.text, message.title); - break; - } - return; - } - - this.splashScreenAdapter.forceRender(); - } - - public findSelectedDatabase(): ViewModels.Database { - if (!this.selectedNode()) { - return null; - } - if (this.selectedNode().nodeKind === "Database") { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id()); - } - return this.findSelectedCollection().database; - } - - public findDatabaseWithId(databaseId: string): ViewModels.Database { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === databaseId); - } - - public isLastNonEmptyDatabase(): boolean { - if (this.isLastDatabase() && this.databases()[0].collections && this.databases()[0].collections().length > 0) { - return true; - } - return false; - } - - public isLastDatabase(): boolean { - if (this.databases().length > 1) { - return false; - } - return true; - } - - public isSelectedDatabaseShared(): boolean { - const database = this.findSelectedDatabase(); - if (!!database) { - return database.offer && !!database.offer(); - } - - return false; - } - - public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void { - if (inputs != null) { - // In development mode, save the iframe message from the portal in session storage. - // This allows webpack hot reload to funciton properly - if (process.env.NODE_ENV === "development") { - sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); - } - - const authorizationToken = inputs.authorizationToken || ""; - const masterKey = inputs.masterKey || ""; - const databaseAccount = inputs.databaseAccount || null; - if (inputs.defaultCollectionThroughput) { - this.collectionCreationDefaults = inputs.defaultCollectionThroughput; - } - this.features(inputs.features); - this.serverId(inputs.serverId); - this.databaseAccount(databaseAccount); - this.subscriptionType(inputs.subscriptionType); - this.hasWriteAccess(inputs.hasWriteAccess); - this.flight(inputs.addCollectionDefaultFlight); - this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription); - this.isAuthWithResourceToken(inputs.isAuthWithresourceToken); - this.setFeatureFlagsFromFlights(inputs.flights); - this._importExplorerConfigComplete = true; - - updateConfigContext({ - BACKEND_ENDPOINT: inputs.extensionEndpoint || "", - ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT) - }); - - updateUserContext({ - authorizationToken, - masterKey, - databaseAccount, - resourceGroup: inputs.resourceGroup, - subscriptionId: inputs.subscriptionId, - subscriptionType: inputs.subscriptionType, - quotaId: inputs.quotaId - }); - TelemetryProcessor.traceSuccess( - Action.LoadDatabaseAccount, - { - resourceId: this.databaseAccount && this.databaseAccount().id, - dataExplorerArea: Constants.Areas.ResourceTree, - databaseAccount: this.databaseAccount && this.databaseAccount() - }, - inputs.loadDatabaseAccountTimestamp - ); - - this.isAccountReady(true); - } - } - - public setFeatureFlagsFromFlights(flights: readonly string[]): void { - if (!flights) { - return; - } - if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) { - this.isMongoIndexingEnabled(true); - } - } - - public findSelectedCollection(): ViewModels.Collection { - return (this.selectedNode().nodeKind === "Collection" - ? this.selectedNode() - : this.selectedNode().collection) as ViewModels.Collection; - } - - // TODO: Refactor below methods, minimize dependencies and add unit tests where necessary - public findSelectedStoredProcedure(): StoredProcedure { - const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); - return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => { - const openedSprocTab = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.StoredProcedures, - tab => tab.node && tab.node.rid === storedProcedure.rid - ); - return ( - storedProcedure.rid === this.selectedNode().rid || - (!!openedSprocTab && openedSprocTab.length > 0 && openedSprocTab[0].isActive()) - ); - }); - } - - public findSelectedUDF(): UserDefinedFunction { - const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); - return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => { - const openedUdfTab = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.UserDefinedFunctions, - tab => tab.node && tab.node.rid === userDefinedFunction.rid - ); - return ( - userDefinedFunction.rid === this.selectedNode().rid || - (!!openedUdfTab && openedUdfTab.length > 0 && openedUdfTab[0].isActive()) - ); - }); - } - - public findSelectedTrigger(): Trigger { - const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); - return _.find(selectedCollection.triggers(), (trigger: Trigger) => { - const openedTriggerTab = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.Triggers, - tab => tab.node && tab.node.rid === trigger.rid - ); - return ( - trigger.rid === this.selectedNode().rid || - (!!openedTriggerTab && openedTriggerTab.length > 0 && openedTriggerTab[0].isActive()) - ); - }); - } - - public closeAllPanes(): void { - this._panes.forEach((pane: ContextualPaneBase) => pane.close()); - } - - public isRunningOnNationalCloud(): boolean { - return ( - this.serverId() === Constants.ServerIds.blackforest || - this.serverId() === Constants.ServerIds.fairfax || - this.serverId() === Constants.ServerIds.mooncake - ); - } - - public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void { - this.commandBarComponentAdapter.onUpdateTabsButtons(buttons); - } - - public signInAad = () => { - TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" }); - sendMessage({ - type: MessageTypes.AadSignIn - }); - }; - - public onSwitchToConnectionString = () => { - $("#connectWithAad").hide(); - $("#connectWithConnectionString").show(); - }; - - public clickHostedAccountSwitch = () => { - sendMessage({ - type: MessageTypes.UpdateAccountSwitch, - click: true - }); - }; - - public clickHostedDirectorySwitch = () => { - sendMessage({ - type: MessageTypes.UpdateDirectoryControl, - click: true - }); - }; - - public refreshDatabaseAccount = () => { - sendMessage({ - type: MessageTypes.RefreshDatabaseAccount - }); - }; - - private refreshAndExpandNewDatabases(newDatabases: ViewModels.Database[]): Q.Promise { - // we reload collections for all databases so the resource tree reflects any collection-level changes - // i.e addition of stored procedures, etc. - const deferred: Q.Deferred = Q.defer(); - let loadCollectionPromises: Q.Promise[] = []; - - // If the user has a lot of databases, only load expanded databases. - const databasesToLoad = - this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand - ? this.databases() - : this.databases().filter(db => db.isDatabaseExpanded()); - - const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree - }); - databasesToLoad.forEach(async (database: ViewModels.Database) => { - await database.loadCollections(); - const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id()); - if (isNewDatabase) { - database.expandDatabase(); - } - this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().id() === database.id()); - }); - - Q.all(loadCollectionPromises).done( - () => { - deferred.resolve(); - TelemetryProcessor.traceSuccess( - Action.LoadCollections, - { dataExplorerArea: Constants.Areas.ResourceTree }, - startKey - ); - }, - (error: any) => { - deferred.reject(error); - TelemetryProcessor.traceFailure( - Action.LoadCollections, - { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ResourceTree, - error: getErrorMessage(error), - errorStack: getErrorStack(error) - }, - startKey - ); - } - ); - return deferred.promise; - } - - // TODO: Abstract this elsewhere - private _openShareDialog: () => void = (): void => { - if (!$("#shareDataAccessFlyout").dialog("instance")) { - const accountMetadataInfo = { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.ShareDialog - }; - const openFullscreenButton = { - text: "Open", - class: "openFullScreenBtn openFullScreenCancelBtn", - click: () => { - TelemetryProcessor.trace( - Action.SelectItem, - ActionModifiers.Mark, - _.extend({}, { description: "Open full screen" }, accountMetadataInfo) - ); - - const hiddenAnchorElement: HTMLAnchorElement = document.createElement("a"); - hiddenAnchorElement.href = this.shareAccessUrl(); - hiddenAnchorElement.target = "_blank"; - $("#shareDataAccessFlyout").dialog("close"); - hiddenAnchorElement.click(); - } - }; - const cancelButton = { - text: "Cancel", - class: "shareCancelButton openFullScreenCancelBtn", - click: () => { - TelemetryProcessor.trace( - Action.SelectItem, - ActionModifiers.Mark, - _.extend({}, { description: "Cancel open full screen" }, accountMetadataInfo) - ); - $("#shareDataAccessFlyout").dialog("close"); - } - }; - $("#shareDataAccessFlyout").dialog({ - autoOpen: false, - buttons: [openFullscreenButton, cancelButton], - closeOnEscape: true, - draggable: false, - dialogClass: "no-close", - position: { my: "right top", at: "right bottom", of: $(".OpenFullScreen") }, - resizable: false, - title: "Open Full Screen", - width: 400, - close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowShareDialogContents(false) - }); - $("#shareDataAccessFlyout").dialog("option", "classes", { - "ui-widget-content": "shareUrlDialog", - "ui-widget-header": "shareUrlTitle", - "ui-dialog-titlebar-close": "shareClose", - "ui-button": "shareCloseIcon", - "ui-button-icon": "cancelIcon", - "ui-icon": "" - }); - $("#shareDataAccessFlyout").dialog("option", "open", (event: Event, ui: JQueryUI.DialogUIParams) => - $(".openFullScreenBtn").focus() - ); - } - $("#shareDataAccessFlyout").dialog("close"); - this.shouldShowShareDialogContents(true); - $("#shareDataAccessFlyout").dialog("open"); - }; - - private _getShareAccessUrlForToken(token: string): string { - if (!token) { - return undefined; - } - - const urlPrefixWithKeyParam: string = `${configContext.hostedExplorerURL}?key=`; - const currentActiveTab = this.tabsManager.activeTab(); - - return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`; - } - - private _initSettings() { - if (!ExplorerSettings.hasSettingsDefined()) { - ExplorerSettings.createDefaultSettings(); - } - } - - public findCollection(databaseId: string, collectionId: string): ViewModels.Collection { - const database: ViewModels.Database = this.databases().find( - (database: ViewModels.Database) => database.id() === databaseId - ); - return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId); - } - - public isLastCollection(): boolean { - let collectionCount = 0; - if (this.databases().length == 0) { - return false; - } - for (let i = 0; i < this.databases().length; i++) { - const database = this.databases()[i]; - collectionCount += database.collections().length; - if (collectionCount > 1) { - return false; - } - } - return true; - } - - private getDeltaDatabases( - updatedDatabaseList: DataModels.Database[] - ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } { - const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { - const databaseExists = _.some( - this.databases(), - (existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id - ); - return !databaseExists; - }); - const databasesToAdd: ViewModels.Database[] = newDatabases.map( - (newDatabase: DataModels.Database) => new Database(this, newDatabase) - ); - - let databasesToDelete: ViewModels.Database[] = []; - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { - const databasePresentInUpdatedList = _.some( - updatedDatabaseList, - (db: DataModels.Database) => db.id === database.id() - ); - if (!databasePresentInUpdatedList) { - databasesToDelete.push(database); - } - }); - - return { toAdd: databasesToAdd, toDelete: databasesToDelete }; - } - - private addDatabasesToList(databases: ViewModels.Database[]): void { - this.databases( - this.databases() - .concat(databases) - .sort((database1, database2) => database1.id().localeCompare(database2.id())) - ); - } - - private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void { - const databasesToKeep: ViewModels.Database[] = []; - - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { - const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id); - if (!shouldRemoveDatabase) { - databasesToKeep.push(database); - } - }); - - this.databases(databasesToKeep); - } - - public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to upload notebook, but notebook is not enabled"; - handleError(error, "Explorer/uploadFile"); - throw new Error(error); - } - - const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); - promise - .then(() => this.resourceTree.triggerRender()) - .catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason)); - return promise; - } - - public async importAndOpen(path: string): Promise { - const name = NotebookUtil.getName(path); - const item = NotebookUtil.createNotebookContentItem(name, path, "file"); - const parent = this.resourceTree.myNotebooksContentRoot; - - if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { - const existingItem = _.find(parent.children, node => node.name === name); - if (existingItem) { - return this.openNotebook(existingItem); - } - - const content = await this.readFile(item); - const uploadedItem = await this.uploadFile(name, content, parent); - return this.openNotebook(uploadedItem); - } - - return Promise.resolve(false); - } - - public async importAndOpenContent(name: string, content: string): Promise { - const parent = this.resourceTree.myNotebooksContentRoot; - - if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { - if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { - this.notebookToImport = undefined; // we don't want to try opening this notebook again - } - - const existingItem = _.find(parent.children, node => node.name === name); - if (existingItem) { - return this.openNotebook(existingItem); - } - - const uploadedItem = await this.uploadFile(name, content, parent); - return this.openNotebook(uploadedItem); - } - - this.notebookToImport = { name, content }; // we'll try opening this notebook later on - return Promise.resolve(false); - } - - public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise { - if (this.notebookManager) { - await this.notebookManager.openPublishNotebookPane( - name, - content, - parentDomElement, - this.isLinkInjectionEnabled() - ); - this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; - this.isPublishNotebookPaneEnabled(true); - } - } - - public copyNotebook(name: string, content: string): void { - if (this.notebookManager) { - this.notebookManager.openCopyNotebookPane(name, content); - this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter; - this.isCopyNotebookPaneEnabled(true); - } - } - - public showOkModalDialog(title: string, msg: string): void { - this._dialogProps({ - isModal: true, - visible: true, - title, - subText: msg, - primaryButtonText: "Close", - secondaryButtonText: undefined, - onPrimaryButtonClick: this._closeModalDialog, - onSecondaryButtonClick: undefined - }); - } - - public showOkCancelModalDialog( - title: string, - msg: string, - okLabel: string, - onOk: () => void, - cancelLabel: string, - onCancel: () => void, - choiceGroupProps?: IChoiceGroupProps, - textFieldProps?: TextFieldProps, - isPrimaryButtonDisabled?: boolean - ): void { - this._dialogProps({ - isModal: true, - visible: true, - title, - subText: msg, - primaryButtonText: okLabel, - secondaryButtonText: cancelLabel, - onPrimaryButtonClick: () => { - this._closeModalDialog(); - onOk && onOk(); - }, - onSecondaryButtonClick: () => { - this._closeModalDialog(); - onCancel && onCancel(); - }, - choiceGroupProps, - textFieldProps, - primaryButtonDisabled: isPrimaryButtonDisabled - }); - } - - /** - * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. - * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. - * Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder - * will not fetch its content if the children array exists (and has only one child which was manually created). - * Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal. - * - * @param name - * @param path - */ - public createNotebookContentItemFile(name: string, path: string): NotebookContentItem { - return NotebookUtil.createNotebookContentItem(name, path, "file"); - } - - public async openNotebook(notebookContentItem: NotebookContentItem): Promise { - if (!notebookContentItem || !notebookContentItem.path) { - throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); - } - - const notebookTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - tab => - (tab as NotebookV2Tab).notebookPath && - FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path) - ) as NotebookV2Tab[]; - let notebookTab = notebookTabs && notebookTabs[0]; - - if (notebookTab) { - this.tabsManager.activateTab(notebookTab); - } else { - const options: NotebookTabOptions = { - account: userContext.databaseAccount, - tabKind: ViewModels.CollectionTabKind.NotebookV2, - node: null, - title: notebookContentItem.name, - tabPath: notebookContentItem.path, - collection: null, - masterKey: userContext.masterKey || "", - hashLocation: "notebooks", - isActive: ko.observable(false), - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: null, - onUpdateTabsButtons: this.onUpdateTabsButtons, - container: this, - notebookContentItem - }; - - try { - const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); - notebookTab = new NotebookTabV2.default(options); - this.tabsManager.activateNewTab(notebookTab); - } catch (reason) { - console.error("Import NotebookV2Tab failed!", reason); - return false; - } - } - - return true; - } - - public renameNotebook(notebookFile: NotebookContentItem): Q.Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to rename notebook, but notebook is not enabled"; - handleError(error, "Explorer/renameNotebook"); - throw new Error(error); - } - - // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab: NotebookV2Tab) => { - return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); - } - ); - if (openedNotebookTabs.length > 0) { - this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); - return Q.reject(); - } - - const originalPath = notebookFile.path; - const result = this.stringInputPane - .openWithOptions({ - errorMessage: "Could not rename notebook", - inProgressMessage: "Renaming notebook to", - successMessage: "Renamed notebook to", - inputLabel: "Enter new notebook name", - paneTitle: "Rename Notebook", - submitButtonLabel: "Rename", - defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"), - onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input) - }) - .then(newNotebookFile => { - const notebookTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath) - ); - notebookTabs.forEach(tab => { - tab.tabTitle(newNotebookFile.name); - tab.tabPath(newNotebookFile.path); - (tab as NotebookV2Tab).notebookPath(newNotebookFile.path); - }); - - return newNotebookFile; - }); - result.then(() => this.resourceTree.triggerRender()); - return result; - } - - public onCreateDirectory(parent: NotebookContentItem): Q.Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create notebook directory, but notebook is not enabled"; - handleError(error, "Explorer/onCreateDirectory"); - throw new Error(error); - } - - const result = this.stringInputPane.openWithOptions({ - errorMessage: "Could not create directory ", - inProgressMessage: "Creating directory ", - successMessage: "Created directory ", - inputLabel: "Enter new directory name", - paneTitle: "Create new directory", - submitButtonLabel: "Create", - defaultInput: "", - onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input) - }); - result.then(() => this.resourceTree.triggerRender()); - return result; - } - - public readFile(notebookFile: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to read file, but notebook is not enabled"; - handleError(error, "Explorer/downloadFile"); - throw new Error(error); - } - - return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path); - } - - public downloadFile(notebookFile: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to download file, but notebook is not enabled"; - handleError(error, "Explorer/downloadFile"); - throw new Error(error); - } - - const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`); - - return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then( - (content: string) => { - const blob = stringToBlob(content, "text/plain"); - if (navigator.msSaveBlob) { - // for IE and Edge - navigator.msSaveBlob(blob, notebookFile.name); - } else { - const downloadLink: HTMLAnchorElement = document.createElement("a"); - const url = URL.createObjectURL(blob); - downloadLink.href = url; - downloadLink.target = "_self"; - downloadLink.download = notebookFile.name; - - // for some reason, FF displays the download prompt only when - // the link is added to the dom so we add and remove it - document.body.appendChild(downloadLink); - downloadLink.click(); - downloadLink.remove(); - } - - clearMessage(); - }, - (error: any) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Could not download notebook ${getErrorMessage(error)}` - ); - - clearMessage(); - } - ); - } - - private async _refreshNotebooksEnabledStateForAccount(): Promise { - const authType = window.authType as AuthType; - if ( - authType === AuthType.EncryptedToken || - authType === AuthType.ResourceToken || - authType === AuthType.MasterKey - ) { - this.isNotebooksEnabledForAccount(false); - return; - } - - const databaseAccount = this.databaseAccount(); - const databaseAccountLocation = databaseAccount && databaseAccount.location.toLowerCase(); - const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`; - const authorizationHeader = getAuthorizationHeader(); - try { - const response = await fetch(disallowedLocationsUri, { - method: "POST", - body: JSON.stringify({ - resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces] - }), - headers: { - [authorizationHeader.header]: authorizationHeader.token, - [Constants.HttpHeaders.contentType]: "application/json" - } - }); - - if (!response.ok) { - throw new Error("Failed to fetch disallowed locations"); - } - - const disallowedLocations: string[] = await response.json(); - if (!disallowedLocations) { - Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount"); - this.isNotebooksEnabledForAccount(true); - return; - } - const isAccountInAllowedLocation = !disallowedLocations.some( - disallowedLocation => disallowedLocation === databaseAccountLocation - ); - this.isNotebooksEnabledForAccount(isAccountInAllowedLocation); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount"); - this.isNotebooksEnabledForAccount(false); - } - } - - public _refreshSparkEnabledStateForAccount = async (): Promise => { - const subscriptionId = userContext.subscriptionId; - const armEndpoint = configContext.ARM_ENDPOINT; - const authType = window.authType as AuthType; - if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { - // explorer is not aware of the database account yet - this.isSparkEnabledForAccount(false); - return; - } - - const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`; - const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); - try { - const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync( - featureUri, - Constants.ArmApiVersions.armFeatures - ); - const isEnabled = - (sparkNotebooksFeature && - sparkNotebooksFeature.properties && - sparkNotebooksFeature.properties.state === "Registered") || - false; - this.isSparkEnabledForAccount(isEnabled); - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount"); - this.isSparkEnabledForAccount(false); - } - }; - - public _isAfecFeatureRegistered = async (featureName: string): Promise => { - const subscriptionId = userContext.subscriptionId; - const armEndpoint = configContext.ARM_ENDPOINT; - const authType = window.authType as AuthType; - if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { - // explorer is not aware of the database account yet - return false; - } - - const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`; - const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); - try { - const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync( - featureUri, - Constants.ArmApiVersions.armFeatures - ); - const isEnabled = - (featureStatus && featureStatus.properties && featureStatus.properties.state === "Registered") || false; - return isEnabled; - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount"); - return false; - } - }; - private refreshNotebookList = async (): Promise => { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - return; - } - - await this.resourceTree.initialize(); - this.notebookManager?.refreshPinnedRepos(); - if (this.notebookToImport) { - this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); - } - }; - - public deleteNotebookFile(item: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to delete notebook file, but notebook is not enabled"; - handleError(error, "Explorer/deleteNotebookFile"); - throw new Error(error); - } - - // Don't delete if tab is open to avoid accidental deletion - const openedNotebookTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.NotebookV2, - (tab: NotebookV2Tab) => { - return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); - } - ); - if (openedNotebookTabs.length > 0) { - this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); - return Promise.reject(); - } - - if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) { - this._dialogProps({ - isModal: true, - visible: true, - title: "Unable to delete file", - subText: "Directory is not empty.", - primaryButtonText: "Close", - secondaryButtonText: undefined, - onPrimaryButtonClick: this._closeModalDialog, - onSecondaryButtonClick: undefined - }); - return Promise.reject(); - } - - return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( - () => { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`); - }, - (reason: any) => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to delete "${item.path}": ${JSON.stringify(reason)}` - ); - } - ); - } - - /** - * This creates a new notebook file, then opens the notebook - */ - public onNewNotebookClicked(parent?: NotebookContentItem): void { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to create new notebook, but notebook is not enabled"; - handleError(error, "Explorer/onNewNotebookClicked"); - throw new Error(error); - } - - parent = parent || this.resourceTree.myNotebooksContentRoot; - - const notificationProgressId = NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.InProgress, - `Creating new notebook in ${parent.path}` - ); - - const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { - databaseAccountName: this.databaseAccount() && this.databaseAccount().name, - defaultExperience: this.defaultExperience && this.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook - }); - - this.notebookManager?.notebookContentClient - .createNewNotebookFile(parent) - .then((newFile: NotebookContentItem) => { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`); - TelemetryProcessor.traceSuccess( - Action.CreateNewNotebook, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook - }, - startKey - ); - return this.openNotebook(newFile); - }) - .then(() => this.resourceTree.triggerRender()) - .catch((error: any) => { - const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); - TelemetryProcessor.traceFailure( - Action.CreateNewNotebook, - { - databaseAccountName: this.databaseAccount().name, - defaultExperience: this.defaultExperience(), - dataExplorerArea: Constants.Areas.Notebook, - error: errorMessage, - errorStack: getErrorStack(error) - }, - startKey - ); - }) - .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId)); - } - - public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void { - parent = parent || this.resourceTree.myNotebooksContentRoot; - - this.uploadFilePane.openWithOptions({ - paneTitle: "Upload file to notebook server", - selectFileInputLabel: "Select file to upload", - errorMessage: "Could not upload file", - inProgressMessage: "Uploading file to notebook server", - successMessage: "Successfully uploaded file to notebook server", - onSubmit: async (file: File): Promise => { - const readFileAsText = (inputFile: File): Promise => { - const reader = new FileReader(); - return new Promise((resolve, reject) => { - reader.onerror = () => { - reader.abort(); - reject(`Problem parsing file: ${inputFile}`); - }; - reader.onload = () => { - resolve(reader.result as string); - }; - reader.readAsText(inputFile); - }); - }; - - const fileContent = await readFileAsText(file); - return this.uploadFile(file.name, fileContent, parent); - }, - extensions: undefined, - submitButtonLabel: "Upload" - }); - } - - public refreshContentItem(item: NotebookContentItem): Promise { - if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { - const error = "Attempt to refresh notebook list, but notebook is not enabled"; - handleError(error, "Explorer/refreshContentItem"); - return Promise.reject(new Error(error)); - } - - return this.notebookManager?.notebookContentClient.updateItemChildren(item); - } - - public getNotebookBasePath(): string { - return this.notebookBasePath(); - } - - public openNotebookTerminal(kind: ViewModels.TerminalKind) { - let title: string; - let hashLocation: string; - - switch (kind) { - case ViewModels.TerminalKind.Default: - title = "Terminal"; - hashLocation = "terminal"; - break; - - case ViewModels.TerminalKind.Mongo: - title = "Mongo Shell"; - hashLocation = "mongo-shell"; - break; - - case ViewModels.TerminalKind.Cassandra: - title = "Cassandra Shell"; - hashLocation = "cassandra-shell"; - break; - - default: - throw new Error("Terminal kind: ${kind} not supported"); - } - - const terminalTabs: TerminalTab[] = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.Terminal, - tab => tab.hashLocation() == hashLocation - ) as TerminalTab[]; - let terminalTab: TerminalTab = terminalTabs && terminalTabs[0]; - - if (terminalTab) { - this.tabsManager.activateTab(terminalTab); - } else { - const newTab = new TerminalTab({ - account: userContext.databaseAccount, - tabKind: ViewModels.CollectionTabKind.Terminal, - node: null, - title: title, - tabPath: title, - collection: null, - hashLocation: hashLocation, - isActive: ko.observable(false), - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: null, - onUpdateTabsButtons: this.onUpdateTabsButtons, - container: this, - kind: kind - }); - - this.tabsManager.activateNewTab(newTab); - } - } - - public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) { - let title: string = "Gallery"; - let hashLocation: string = "gallery"; - - const galleryTabs = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.Gallery, - tab => tab.hashLocation() == hashLocation - ); - let galleryTab = galleryTabs && galleryTabs[0]; - - if (galleryTab) { - this.tabsManager.activateTab(galleryTab); - } else { - if (!this.galleryTab) { - this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab"); - } - - const newTab = new this.galleryTab.default({ - // GalleryTabOptions - account: userContext.databaseAccount, - container: this, - junoClient: this.notebookManager?.junoClient, - notebookUrl, - galleryItem, - isFavorite, - // TabOptions - tabKind: ViewModels.CollectionTabKind.Gallery, - title: title, - tabPath: title, - documentClientUtility: null, - isActive: ko.observable(false), - hashLocation: hashLocation, - onUpdateTabsButtons: this.onUpdateTabsButtons, - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: null - }); - - this.tabsManager.activateNewTab(newTab); - } - } - - public async openNotebookViewer(notebookUrl: string) { - const title = path.basename(notebookUrl); - const hashLocation = notebookUrl; - - if (!this.notebookViewerTab) { - this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab"); - } - - const notebookViewerTabModule = this.notebookViewerTab; - - let isNotebookViewerOpen = (tab: TabsBase) => { - const notebookViewerTab = tab as typeof notebookViewerTabModule.default; - return notebookViewerTab.notebookUrl === notebookUrl; - }; - - const notebookViewerTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2, tab => { - return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab); - }); - let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0]; - - if (notebookViewerTab) { - this.tabsManager.activateNewTab(notebookViewerTab); - } else { - notebookViewerTab = new this.notebookViewerTab.default({ - account: userContext.databaseAccount, - tabKind: ViewModels.CollectionTabKind.NotebookViewer, - node: null, - title: title, - tabPath: title, - documentClientUtility: null, - collection: null, - hashLocation: hashLocation, - isActive: ko.observable(false), - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: null, - onUpdateTabsButtons: this.onUpdateTabsButtons, - container: this, - notebookUrl - }); - - this.tabsManager.activateNewTab(notebookViewerTab); - } - } - - public onNewCollectionClicked(): void { - if (this.isPreferredApiCassandra()) { - this.cassandraAddCollectionPane.open(); - } else { - this.addCollectionPane.open(this.selectedDatabaseId()); - } - document.getElementById("linkAddCollection").focus(); - } - - private refreshCommandBarButtons(): void { - const activeTab = this.tabsManager.activeTab(); - if (activeTab) { - activeTab.onActivate(); // TODO only update tabs buttons? - } else { - this.onUpdateTabsButtons([]); - } - } - - private getTokenRefreshInterval(token: string): number { - let tokenRefreshInterval = Constants.ClientDefaults.arcadiaTokenRefreshInterval; - if (!token) { - return tokenRefreshInterval; - } - - try { - const tokenPayload = decryptJWTToken(this.arcadiaToken()); - if (tokenPayload && tokenPayload.hasOwnProperty("exp")) { - const expirationTime = tokenPayload.exp as number; // seconds since unix epoch - const now = new Date().getTime() / 1000; - const tokenExpirationIntervalInMs = (expirationTime - now) * 1000; - if (tokenExpirationIntervalInMs < tokenRefreshInterval) { - tokenRefreshInterval = - tokenExpirationIntervalInMs - Constants.ClientDefaults.arcadiaTokenRefreshIntervalPaddingMs; - } - } - return tokenRefreshInterval; - } catch (error) { - Logger.logError(getErrorMessage(error), "Explorer/getTokenRefreshInterval"); - return tokenRefreshInterval; - } - } - - private _setLoadingStatusText(text: string, title: string = "Welcome to Azure Cosmos DB") { - if (!text) { - return; - } - - const loadingText = document.getElementById("explorerLoadingStatusText"); - if (!loadingText) { - Logger.logError( - "getElementById('explorerLoadingStatusText') failed to find element", - "Explorer/_setLoadingStatusText" - ); - return; - } - loadingText.innerHTML = text; - - const loadingTitle = document.getElementById("explorerLoadingStatusTitle"); - if (!loadingTitle) { - Logger.logError( - "getElementById('explorerLoadingStatusTitle') failed to find element", - "Explorer/_setLoadingStatusText" - ); - } else { - loadingTitle.innerHTML = title; - } - } - - private _openSetupNotebooksPaneForQuickstart(): void { - const title = "Enable Notebooks (Preview)"; - const description = - "You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account."; - - this.setupNotebooksPane.openWithTitleAndDescription(title, description); - } - - public async handleOpenFileAction(path: string): Promise { - if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(this.databaseAccount()))) { - this.closeAllPanes(); - this._openSetupNotebooksPaneForQuickstart(); - } - - // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb - // when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly - // calling GitHub. For now convert this url to a raw url and download content. - const gitHubInfo = fromContentUri(path); - if (gitHubInfo) { - const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path); - const response = await fetch(rawUrl); - if (response.status === Constants.HttpStatusCodes.OK) { - this.notebookToImport = { - name: NotebookUtil.getName(path), - content: await response.text() - }; - - this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); - } - } - } - - public async loadSelectedDatabaseOffer(): Promise { - const database = this.findSelectedDatabase(); - await database?.loadOffer(); - } - - public async loadDatabaseOffers(): Promise { - await Promise.all( - this.databases()?.map(async (database: ViewModels.Database) => { - await database.loadOffer(); - }) - ); - } - - public isFirstResourceCreated(): boolean { - const databases: ViewModels.Database[] = this.databases(); - - if (!databases || databases.length === 0) { - return false; - } - - return databases.some(database => { - // user has created at least one collection - if (database.collections()?.length > 0) { - return true; - } - // user has created a database with shared throughput - if (database.offer()) { - return true; - } - // use has created an empty database without shared throughput - return false; - }); - } -} +import * as ComponentRegisterer from "./ComponentRegisterer"; +import * as Constants from "../Common/Constants"; +import * as DataModels from "../Contracts/DataModels"; +import * as ko from "knockout"; +import * as MostRecentActivity from "./MostRecentActivity/MostRecentActivity"; +import * as path from "path"; +import * as SharedConstants from "../Shared/Constants"; +import * as ViewModels from "../Contracts/ViewModels"; +import _ from "underscore"; +import AddCollectionPane from "./Panes/AddCollectionPane"; +import AddDatabasePane from "./Panes/AddDatabasePane"; +import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane"; +import AuthHeadersUtil from "../Platform/Hosted/Authorization"; +import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; +import Database from "./Tree/Database"; +import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; +import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane"; +import { readCollection } from "../Common/dataAccess/readCollection"; +import { readDatabases } from "../Common/dataAccess/readDatabases"; +import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; +import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; +import GraphStylingPane from "./Panes/GraphStylingPane"; +import hasher from "hasher"; +import NewVertexPane from "./Panes/NewVertexPane"; +import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; +import Q from "q"; +import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; +import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; +import TerminalTab from "./Tabs/TerminalTab"; +import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; +import { ActionContracts, MessageTypes } from "../Contracts/ExplorerContracts"; +import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager"; +import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker"; +import { AuthType } from "../AuthType"; +import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; +import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane"; +import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; +import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter"; +import { configContext, Platform, updateConfigContext } from "../ConfigContext"; +import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent"; +import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils"; +import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; +import { DialogComponentAdapter } from "./Controls/DialogReactComponent/DialogComponentAdapter"; +import { DialogProps, TextFieldProps } from "./Controls/DialogReactComponent/DialogComponent"; +import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane"; +import { ExplorerMetrics } from "../Common/Constants"; +import { ExplorerSettings } from "../Shared/ExplorerSettings"; +import { FileSystemUtil } from "./Notebook/FileSystemUtil"; +import { handleOpenAction } from "./OpenActions"; +import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; +import { IGalleryItem } from "../Juno/JunoClient"; +import { LoadQueryPane } from "./Panes/LoadQueryPane"; +import * as Logger from "../Common/Logger"; +import { sendMessage, sendCachedDataMessage, handleCachedDataMessage } from "../Common/MessageHandler"; +import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; +import { NotebookUtil } from "./Notebook/NotebookUtil"; +import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; +import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter"; +import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; +import { QueriesClient } from "../Common/QueriesClient"; +import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; +import { RenewAdHocAccessPane } from "./Panes/RenewAdHocAccessPane"; +import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; +import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; +import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; +import { RouteHandler } from "../RouteHandlers/RouteHandler"; +import { SaveQueryPane } from "./Panes/SaveQueryPane"; +import { SettingsPane } from "./Panes/SettingsPane"; +import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane"; +import { SplashScreenComponentAdapter } from "./SplashScreen/SplashScreenComponentApdapter"; +import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter"; +import { StringInputPane } from "./Panes/StringInputPane"; +import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane"; +import { TabsManager } from "./Tabs/TabsManager"; +import { UploadFilePane } from "./Panes/UploadFilePane"; +import { UploadItemsPane } from "./Panes/UploadItemsPane"; +import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter"; +import { ReactAdapter } from "../Bindings/ReactBindingHandler"; +import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils"; +import UserDefinedFunction from "./Tree/UserDefinedFunction"; +import StoredProcedure from "./Tree/StoredProcedure"; +import Trigger from "./Tree/Trigger"; +import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; +import TabsBase from "./Tabs/TabsBase"; +import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent"; +import { updateUserContext, userContext } from "../UserContext"; +import { stringToBlob } from "../Utils/BlobUtils"; +import { IChoiceGroupProps } from "office-ui-fabric-react"; +import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils"; +import { SubscriptionType } from "../Contracts/SubscriptionType"; + +BindingHandlersRegisterer.registerBindingHandlers(); +// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import +var tmp = ComponentRegisterer; + +enum ShareAccessToggleState { + ReadWrite, + Read +} + +interface AdHocAccessData { + readWriteUrl: string; + readUrl: string; +} + +export default class Explorer { + public flight: ko.Observable = ko.observable( + SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight + ); + + public addCollectionText: ko.Observable; + public addDatabaseText: ko.Observable; + public collectionTitle: ko.Observable; + public deleteCollectionText: ko.Observable; + public deleteDatabaseText: ko.Observable; + public collectionTreeNodeAltText: ko.Observable; + public refreshTreeTitle: ko.Observable; + public hasWriteAccess: ko.Observable; + public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth; + + public databaseAccount: ko.Observable; + public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; + public subscriptionType: ko.Observable; + public defaultExperience: ko.Observable; + public isPreferredApiDocumentDB: ko.Computed; + public isPreferredApiCassandra: ko.Computed; + public isPreferredApiMongoDB: ko.Computed; + public isPreferredApiGraph: ko.Computed; + public isPreferredApiTable: ko.Computed; + public isFixedCollectionWithSharedThroughputSupported: ko.Computed; + public isEnableMongoCapabilityPresent: ko.Computed; + public isServerlessEnabled: ko.Computed; + public isAccountReady: ko.Observable; + public canSaveQueries: ko.Computed; + public features: ko.Observable; + public serverId: ko.Observable; + public isTryCosmosDBSubscription: ko.Observable; + public queriesClient: QueriesClient; + public tableDataClient: TableDataClient; + public splitter: Splitter; + public mostRecentActivity: MostRecentActivity.MostRecentActivity; + + // Notification Console + public notificationConsoleData: ko.ObservableArray; + public isNotificationConsoleExpanded: ko.Observable; + + // Panes + public contextPanes: ContextualPaneBase[]; + + // Resource Tree + public databases: ko.ObservableArray; + public nonSystemDatabases: ko.Computed; + public selectedDatabaseId: ko.Computed; + public selectedCollectionId: ko.Computed; + public isLeftPaneExpanded: ko.Observable; + public selectedNode: ko.Observable; + public isRefreshingExplorer: ko.Observable; + private resourceTree: ResourceTreeAdapter; + + // Resource Token + public resourceTokenDatabaseId: ko.Observable; + public resourceTokenCollectionId: ko.Observable; + public resourceTokenCollection: ko.Observable; + public resourceTokenPartitionKey: ko.Observable; + public isAuthWithResourceToken: ko.Observable; + public isResourceTokenCollectionNodeSelected: ko.Computed; + private resourceTreeForResourceToken: ResourceTreeAdapterForResourceToken; + + // Tabs + public isTabsContentExpanded: ko.Observable; + public galleryTab: any; + public notebookViewerTab: any; + public tabsManager: TabsManager; + + // Contextual panes + public addDatabasePane: AddDatabasePane; + public addCollectionPane: AddCollectionPane; + public deleteCollectionConfirmationPane: DeleteCollectionConfirmationPane; + public deleteDatabaseConfirmationPane: DeleteDatabaseConfirmationPane; + public graphStylingPane: GraphStylingPane; + public addTableEntityPane: AddTableEntityPane; + public editTableEntityPane: EditTableEntityPane; + public tableColumnOptionsPane: TableColumnOptionsPane; + public querySelectPane: QuerySelectPane; + public newVertexPane: NewVertexPane; + public cassandraAddCollectionPane: CassandraAddCollectionPane; + public settingsPane: SettingsPane; + public executeSprocParamsPane: ExecuteSprocParamsPane; + public renewAdHocAccessPane: RenewAdHocAccessPane; + public uploadItemsPane: UploadItemsPane; + public uploadItemsPaneAdapter: UploadItemsPaneAdapter; + public loadQueryPane: LoadQueryPane; + public saveQueryPane: ContextualPaneBase; + public browseQueriesPane: BrowseQueriesPane; + public uploadFilePane: UploadFilePane; + public stringInputPane: StringInputPane; + public setupNotebooksPane: SetupNotebooksPane; + public gitHubReposPane: ContextualPaneBase; + public publishNotebookPaneAdapter: ReactAdapter; + public copyNotebookPaneAdapter: ReactAdapter; + + // features + public isGalleryPublishEnabled: ko.Computed; + public isLinkInjectionEnabled: ko.Computed; + public isGitHubPaneEnabled: ko.Observable; + public isPublishNotebookPaneEnabled: ko.Observable; + public isCopyNotebookPaneEnabled: ko.Observable; + public isHostedDataExplorerEnabled: ko.Computed; + public isRightPanelV2Enabled: ko.Computed; + public isMongoIndexingEnabled: ko.Observable; + public canExceedMaximumValue: ko.Computed; + public isAutoscaleDefaultEnabled: ko.Observable; + + public shouldShowShareDialogContents: ko.Observable; + public shareAccessData: ko.Observable; + public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise; + public renewTokenError: ko.Observable; + public tokenForRenewal: ko.Observable; + public shareAccessToggleState: ko.Observable; + public shareAccessUrl: ko.Observable; + public shareUrlCopyHelperText: ko.Observable; + public shareTokenCopyHelperText: ko.Observable; + public shouldShowDataAccessExpiryDialog: ko.Observable; + public shouldShowContextSwitchPrompt: ko.Observable; + public isSchemaEnabled: ko.Computed; + + // Notebooks + public isNotebookEnabled: ko.Observable; + public isNotebooksEnabledForAccount: ko.Observable; + public notebookServerInfo: ko.Observable; + public notebookWorkspaceManager: NotebookWorkspaceManager; + public sparkClusterConnectionInfo: ko.Observable; + public isSparkEnabled: ko.Observable; + public isSparkEnabledForAccount: ko.Observable; + public arcadiaToken: ko.Observable; + public arcadiaWorkspaces: ko.ObservableArray; + public hasStorageAnalyticsAfecFeature: ko.Observable; + public isSynapseLinkUpdating: ko.Observable; + public memoryUsageInfo: ko.Observable; + public notebookManager?: any; // This is dynamically loaded + + private _panes: ContextualPaneBase[] = []; + private _importExplorerConfigComplete: boolean = false; + private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = database => false; + private _isInitializingNotebooks: boolean; + private _isInitializingSparkConnectionInfo: boolean; + private notebookBasePath: ko.Observable; + private _arcadiaManager: ArcadiaResourceManager; + private notebookToImport: { + name: string; + content: string; + }; + + // React adapters + private commandBarComponentAdapter: CommandBarComponentAdapter; + private splashScreenAdapter: SplashScreenComponentAdapter; + private notificationConsoleComponentAdapter: NotificationConsoleComponentAdapter; + private dialogComponentAdapter: DialogComponentAdapter; + private _dialogProps: ko.Observable; + private addSynapseLinkDialog: DialogComponentAdapter; + private _addSynapseLinkDialogProps: ko.Observable; + + private static readonly MaxNbDatabasesToAutoExpand = 5; + + constructor() { + const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { + dataExplorerArea: Constants.Areas.ResourceTree + }); + this.addCollectionText = ko.observable("New Collection"); + this.addDatabaseText = ko.observable("New Database"); + this.hasWriteAccess = ko.observable(true); + this.collectionTitle = ko.observable("Collections"); + this.collectionTreeNodeAltText = ko.observable("Collection"); + this.deleteCollectionText = ko.observable("Delete Collection"); + this.deleteDatabaseText = ko.observable("Delete Database"); + this.refreshTreeTitle = ko.observable("Refresh collections"); + + this.databaseAccount = ko.observable(); + this.subscriptionType = ko.observable(SharedConstants.CollectionCreation.DefaultSubscriptionType); + let firstInitialization = true; + this.isRefreshingExplorer = ko.observable(true); + this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => { + if (!isRefreshing && firstInitialization) { + // set focus on first element + firstInitialization = false; + try { + document.getElementById("createNewContainerCommandButton").parentElement.parentElement.focus(); + } catch (e) { + Logger.logWarning( + "getElementById('createNewContainerCommandButton') failed to find element", + "Explorer/this.isRefreshingExplorer.subscribe" + ); + } + } + }); + this.isAccountReady = ko.observable(false); + this._isInitializingNotebooks = false; + this._isInitializingSparkConnectionInfo = false; + this.arcadiaToken = ko.observable(); + this.arcadiaToken.subscribe((token: string) => { + if (token) { + const notebookTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2); + (notebookTabs || []).forEach((tab: NotebookV2Tab) => { + tab.reconfigureServiceEndpoints(); + }); + } + }); + this.isNotebooksEnabledForAccount = ko.observable(false); + this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); + this.isSparkEnabledForAccount = ko.observable(false); + this.isSparkEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); + this.hasStorageAnalyticsAfecFeature = ko.observable(false); + this.hasStorageAnalyticsAfecFeature.subscribe((enabled: boolean) => this.refreshCommandBarButtons()); + this.isSynapseLinkUpdating = ko.observable(false); + this.isAccountReady.subscribe(async (isAccountReady: boolean) => { + if (isAccountReady) { + this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); + RouteHandler.getInstance().initHandler(); + this.notebookWorkspaceManager = new NotebookWorkspaceManager(); + this.arcadiaWorkspaces = ko.observableArray(); + this._arcadiaManager = new ArcadiaResourceManager(); + this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered => + this.hasStorageAnalyticsAfecFeature(isRegistered) + ); + Promise.all([this._refreshNotebooksEnabledStateForAccount(), this._refreshSparkEnabledStateForAccount()]).then( + async () => { + this.isNotebookEnabled( + !this.isAuthWithResourceToken() && + ((await this._containsDefaultNotebookWorkspace(this.databaseAccount())) || + this.isFeatureEnabled(Constants.Features.enableNotebooks)) + ); + + TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { + isNotebookEnabled: this.isNotebookEnabled(), + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook + }); + + if (this.isNotebookEnabled()) { + await this.initNotebooks(this.databaseAccount()); + const workspaces = await this._getArcadiaWorkspaces(); + this.arcadiaWorkspaces(workspaces); + } else if (this.notebookToImport) { + // if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane + this._openSetupNotebooksPaneForQuickstart(); + } + + this.isSparkEnabled( + (this.isNotebookEnabled() && + this.isSparkEnabledForAccount() && + this.arcadiaWorkspaces() && + this.arcadiaWorkspaces().length > 0) || + this.isFeatureEnabled(Constants.Features.enableSpark) + ); + if (this.isSparkEnabled()) { + const pollArcadiaTokenRefresh = async () => { + this.arcadiaToken(await this.getArcadiaToken()); + setTimeout(() => pollArcadiaTokenRefresh(), this.getTokenRefreshInterval(this.arcadiaToken())); + }; + await pollArcadiaTokenRefresh(); + } + } + ); + } + }); + this.memoryUsageInfo = ko.observable(); + + this.features = ko.observable(); + this.serverId = ko.observable(); + this.queriesClient = new QueriesClient(this); + this.isTryCosmosDBSubscription = ko.observable(false); + + this.resourceTokenDatabaseId = ko.observable(); + this.resourceTokenCollectionId = ko.observable(); + this.resourceTokenCollection = ko.observable(); + this.resourceTokenPartitionKey = ko.observable(); + this.isAuthWithResourceToken = ko.observable(false); + + this.shareAccessData = ko.observable({ + readWriteUrl: undefined, + readUrl: undefined + }); + this.tokenForRenewal = ko.observable(""); + this.renewTokenError = ko.observable(""); + this.shareAccessUrl = ko.observable(); + this.shareUrlCopyHelperText = ko.observable("Click to copy"); + this.shareTokenCopyHelperText = ko.observable("Click to copy"); + this.shareAccessToggleState = ko.observable(ShareAccessToggleState.ReadWrite); + this.shareAccessToggleState.subscribe((toggleState: ShareAccessToggleState) => { + if (toggleState === ShareAccessToggleState.ReadWrite) { + this.shareAccessUrl(this.shareAccessData && this.shareAccessData().readWriteUrl); + } else { + this.shareAccessUrl(this.shareAccessData && this.shareAccessData().readUrl); + } + }); + this.shouldShowShareDialogContents = ko.observable(false); + this.shouldShowDataAccessExpiryDialog = ko.observable(false); + this.shouldShowContextSwitchPrompt = ko.observable(false); + this.isGalleryPublishEnabled = ko.computed(() => + this.isFeatureEnabled(Constants.Features.enableGalleryPublish) + ); + this.isLinkInjectionEnabled = ko.computed(() => + this.isFeatureEnabled(Constants.Features.enableLinkInjection) + ); + this.isGitHubPaneEnabled = ko.observable(false); + this.isMongoIndexingEnabled = ko.observable(false); + this.isPublishNotebookPaneEnabled = ko.observable(false); + this.isCopyNotebookPaneEnabled = ko.observable(false); + + this.canExceedMaximumValue = ko.computed(() => + this.isFeatureEnabled(Constants.Features.canExceedMaximumValue) + ); + + this.isSchemaEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableSchema)); + this.isNotificationConsoleExpanded = ko.observable(false); + + this.isAutoscaleDefaultEnabled = ko.observable(false); + + this.databases = ko.observableArray(); + this.canSaveQueries = ko.computed(() => { + const savedQueriesDatabase: ViewModels.Database = _.find( + this.databases(), + (database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName + ); + if (!savedQueriesDatabase) { + return false; + } + const savedQueriesCollection: ViewModels.Collection = + savedQueriesDatabase && + _.find( + savedQueriesDatabase.collections(), + (collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName + ); + if (!savedQueriesCollection) { + return false; + } + return true; + }); + this.isLeftPaneExpanded = ko.observable(true); + this.selectedNode = ko.observable(); + this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { + // Make sure switching tabs restores tabs display + this.isTabsContentExpanded(false); + }); + this.isResourceTokenCollectionNodeSelected = ko.computed(() => { + return ( + this.selectedNode() && + this.resourceTokenCollection() && + this.selectedNode().id() === this.resourceTokenCollection().id() + ); + }); + + const splitterBounds: SplitterBounds = { + min: ExplorerMetrics.SplitterMinWidth, + max: ExplorerMetrics.SplitterMaxWidth + }; + this.splitter = new Splitter({ + splitterId: "h_splitter1", + leftId: "resourcetree", + bounds: splitterBounds, + direction: SplitterDirection.Vertical + }); + this.notificationConsoleData = ko.observableArray([]); + this.defaultExperience = ko.observable(); + this.databaseAccount.subscribe(databaseAccount => { + const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount( + databaseAccount + ); + this.defaultExperience(defaultExperience); + updateUserContext({ + defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience) + }); + }); + + this.isPreferredApiDocumentDB = ko.computed(() => { + const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; + return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.DocumentDB.toLowerCase(); + }); + + this.isPreferredApiCassandra = ko.computed(() => { + const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; + return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase(); + }); + this.isPreferredApiGraph = ko.computed(() => { + const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; + return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Graph.toLowerCase(); + }); + + this.isPreferredApiTable = ko.computed(() => { + const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; + return defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Table.toLowerCase(); + }); + + this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => { + if (this.isFeatureEnabled(Constants.Features.enableFixedCollectionWithSharedThroughput)) { + return true; + } + + if (this.databaseAccount && !this.databaseAccount()) { + return false; + } + + return this.isEnableMongoCapabilityPresent(); + }); + + this.isServerlessEnabled = ko.computed( + () => + this.databaseAccount && + this.databaseAccount()?.properties?.capabilities?.find( + item => item.name === Constants.CapabilityNames.EnableServerless + ) !== undefined + ); + + this.isPreferredApiMongoDB = ko.computed(() => { + const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; + if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) { + return true; + } + + if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase()) { + return true; + } + + if ( + this.databaseAccount && + this.databaseAccount() && + this.databaseAccount().kind.toLowerCase() === Constants.AccountKind.MongoDB + ) { + return true; + } + + return false; + }); + + this.isEnableMongoCapabilityPresent = ko.computed(() => { + const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities; + if (!capabilities) { + return false; + } + + for (let i = 0; i < capabilities.length; i++) { + if (typeof capabilities[i] === "object" && capabilities[i].name === Constants.CapabilityNames.EnableMongo) { + return true; + } + } + + return false; + }); + + this.isHostedDataExplorerEnabled = ko.computed( + () => + configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph() + ); + this.isRightPanelV2Enabled = ko.computed(() => + this.isFeatureEnabled(Constants.Features.enableRightPanelV2) + ); + this.defaultExperience.subscribe((defaultExperience: string) => { + if ( + defaultExperience && + defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase() + ) { + this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => { + return database.id() === "system"; + }; + } + }); + + this.selectedDatabaseId = ko.computed(() => { + const selectedNode = this.selectedNode(); + if (!selectedNode) { + return ""; + } + + switch (selectedNode.nodeKind) { + case "Collection": + return (selectedNode as ViewModels.CollectionBase).databaseId || ""; + case "Database": + return selectedNode.id() || ""; + case "DocumentId": + case "StoredProcedure": + case "Trigger": + case "UserDefinedFunction": + return selectedNode.collection.databaseId || ""; + default: + return ""; + } + }); + + this.nonSystemDatabases = ko.computed(() => { + return this.databases().filter((database: ViewModels.Database) => !this._isSystemDatabasePredicate(database)); + }); + + this.addDatabasePane = new AddDatabasePane({ + id: "adddatabasepane", + visible: ko.observable(false), + + container: this + }); + + this.addCollectionPane = new AddCollectionPane({ + isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()), + id: "addcollectionpane", + visible: ko.observable(false), + + container: this + }); + + this.deleteCollectionConfirmationPane = new DeleteCollectionConfirmationPane({ + id: "deletecollectionconfirmationpane", + visible: ko.observable(false), + + container: this + }); + + this.deleteDatabaseConfirmationPane = new DeleteDatabaseConfirmationPane({ + id: "deletedatabaseconfirmationpane", + visible: ko.observable(false), + + container: this + }); + + this.graphStylingPane = new GraphStylingPane({ + id: "graphstylingpane", + visible: ko.observable(false), + + container: this + }); + + this.addTableEntityPane = new AddTableEntityPane({ + id: "addtableentitypane", + visible: ko.observable(false), + + container: this + }); + + this.editTableEntityPane = new EditTableEntityPane({ + id: "edittableentitypane", + visible: ko.observable(false), + + container: this + }); + + this.tableColumnOptionsPane = new TableColumnOptionsPane({ + id: "tablecolumnoptionspane", + visible: ko.observable(false), + + container: this + }); + + this.querySelectPane = new QuerySelectPane({ + id: "queryselectpane", + visible: ko.observable(false), + + container: this + }); + + this.newVertexPane = new NewVertexPane({ + id: "newvertexpane", + visible: ko.observable(false), + + container: this + }); + + this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ + id: "cassandraaddcollectionpane", + visible: ko.observable(false), + + container: this + }); + + this.settingsPane = new SettingsPane({ + id: "settingspane", + visible: ko.observable(false), + + container: this + }); + + this.executeSprocParamsPane = new ExecuteSprocParamsPane({ + id: "executesprocparamspane", + visible: ko.observable(false), + + container: this + }); + + this.renewAdHocAccessPane = new RenewAdHocAccessPane({ + id: "renewadhocaccesspane", + visible: ko.observable(false), + + container: this + }); + + this.uploadItemsPane = new UploadItemsPane({ + id: "uploaditemspane", + visible: ko.observable(false), + + container: this + }); + + this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this); + + this.loadQueryPane = new LoadQueryPane({ + id: "loadquerypane", + visible: ko.observable(false), + + container: this + }); + + this.saveQueryPane = new SaveQueryPane({ + id: "savequerypane", + visible: ko.observable(false), + + container: this + }); + + this.browseQueriesPane = new BrowseQueriesPane({ + id: "browsequeriespane", + visible: ko.observable(false), + + container: this + }); + + this.uploadFilePane = new UploadFilePane({ + id: "uploadfilepane", + visible: ko.observable(false), + + container: this + }); + + this.stringInputPane = new StringInputPane({ + id: "stringinputpane", + visible: ko.observable(false), + + container: this + }); + + this.setupNotebooksPane = new SetupNotebooksPane({ + id: "setupnotebookspane", + visible: ko.observable(false), + + container: this + }); + + this.tabsManager = new TabsManager(); + + this._panes = [ + this.addDatabasePane, + this.addCollectionPane, + this.deleteCollectionConfirmationPane, + this.deleteDatabaseConfirmationPane, + this.graphStylingPane, + this.addTableEntityPane, + this.editTableEntityPane, + this.tableColumnOptionsPane, + this.querySelectPane, + this.newVertexPane, + this.cassandraAddCollectionPane, + this.settingsPane, + this.executeSprocParamsPane, + this.renewAdHocAccessPane, + this.uploadItemsPane, + this.loadQueryPane, + this.saveQueryPane, + this.browseQueriesPane, + this.uploadFilePane, + this.stringInputPane, + this.setupNotebooksPane + ]; + this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); + this.isTabsContentExpanded = ko.observable(false); + + document.addEventListener( + "contextmenu", + function(e) { + e.preventDefault(); + }, + false + ); + + $(function() { + $(document.body).click(() => $(".commandDropdownContainer").hide()); + }); + + // TODO move this to API customization class + this.defaultExperience.subscribe(defaultExperience => { + const defaultExperienceNormalizedString = ( + defaultExperience || Constants.DefaultAccountExperience.Default + ).toLowerCase(); + + switch (defaultExperienceNormalizedString) { + case Constants.DefaultAccountExperience.DocumentDB.toLowerCase(): + this.addCollectionText("New Container"); + this.addDatabaseText("New Database"); + this.collectionTitle("SQL API"); + this.collectionTreeNodeAltText("Container"); + this.deleteCollectionText("Delete Container"); + this.deleteDatabaseText("Delete Database"); + this.addCollectionPane.title("Add Container"); + this.addCollectionPane.collectionIdTitle("Container id"); + this.addCollectionPane.collectionWithThroughputInSharedTitle( + "Provision dedicated throughput for this container" + ); + this.deleteCollectionConfirmationPane.title("Delete Container"); + this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the container id"); + this.refreshTreeTitle("Refresh containers"); + break; + case Constants.DefaultAccountExperience.MongoDB.toLowerCase(): + case Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase(): + this.addCollectionText("New Collection"); + this.addDatabaseText("New Database"); + this.collectionTitle("Collections"); + this.collectionTreeNodeAltText("Collection"); + this.deleteCollectionText("Delete Collection"); + this.deleteDatabaseText("Delete Database"); + this.addCollectionPane.title("Add Collection"); + this.addCollectionPane.collectionIdTitle("Collection id"); + this.addCollectionPane.collectionWithThroughputInSharedTitle( + "Provision dedicated throughput for this collection" + ); + this.refreshTreeTitle("Refresh collections"); + break; + case Constants.DefaultAccountExperience.Graph.toLowerCase(): + this.addCollectionText("New Graph"); + this.addDatabaseText("New Database"); + this.deleteCollectionText("Delete Graph"); + this.deleteDatabaseText("Delete Database"); + this.collectionTitle("Gremlin API"); + this.collectionTreeNodeAltText("Graph"); + this.addCollectionPane.title("Add Graph"); + this.addCollectionPane.collectionIdTitle("Graph id"); + this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph"); + this.deleteCollectionConfirmationPane.title("Delete Graph"); + this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the graph id"); + this.refreshTreeTitle("Refresh graphs"); + break; + case Constants.DefaultAccountExperience.Table.toLowerCase(): + this.addCollectionText("New Table"); + this.addDatabaseText("New Database"); + this.deleteCollectionText("Delete Table"); + this.deleteDatabaseText("Delete Database"); + this.collectionTitle("Azure Table API"); + this.collectionTreeNodeAltText("Table"); + this.addCollectionPane.title("Add Table"); + this.addCollectionPane.collectionIdTitle("Table id"); + this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); + this.refreshTreeTitle("Refresh tables"); + this.addTableEntityPane.title("Add Table Entity"); + this.editTableEntityPane.title("Edit Table Entity"); + this.deleteCollectionConfirmationPane.title("Delete Table"); + this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); + this.tableDataClient = new TablesAPIDataClient(); + break; + case Constants.DefaultAccountExperience.Cassandra.toLowerCase(): + this.addCollectionText("New Table"); + this.addDatabaseText("New Keyspace"); + this.deleteCollectionText("Delete Table"); + this.deleteDatabaseText("Delete Keyspace"); + this.collectionTitle("Cassandra API"); + this.collectionTreeNodeAltText("Table"); + this.addCollectionPane.title("Add Table"); + this.addCollectionPane.collectionIdTitle("Table id"); + this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); + this.refreshTreeTitle("Refresh tables"); + this.addTableEntityPane.title("Add Table Row"); + this.editTableEntityPane.title("Edit Table Row"); + this.deleteCollectionConfirmationPane.title("Delete Table"); + this.deleteCollectionConfirmationPane.collectionIdConfirmationText("Confirm by typing the table id"); + this.deleteDatabaseConfirmationPane.title("Delete Keyspace"); + this.deleteDatabaseConfirmationPane.databaseIdConfirmationText("Confirm by typing the keyspace id"); + this.tableDataClient = new CassandraAPIDataClient(); + break; + } + }); + + this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); + this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this); + + this._initSettings(); + + TelemetryProcessor.traceSuccess( + Action.InitializeDataExplorer, + { dataExplorerArea: Constants.Areas.ResourceTree }, + startKey + ); + + this.isNotebookEnabled = ko.observable(false); + this.isNotebookEnabled.subscribe(async () => { + if (!this.notebookManager) { + const notebookManagerModule = await import( + /* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager" + ); + this.notebookManager = new notebookManagerModule.default(); + this.notebookManager.initialize({ + container: this, + dialogProps: this._dialogProps, + notebookBasePath: this.notebookBasePath, + resourceTree: this.resourceTree, + refreshCommandBarButtons: () => this.refreshCommandBarButtons(), + refreshNotebookList: () => this.refreshNotebookList() + }); + + this.gitHubReposPane = this.notebookManager.gitHubReposPane; + this.isGitHubPaneEnabled(true); + } + + this.refreshCommandBarButtons(); + this.refreshNotebookList(); + }); + + this.isSparkEnabled = ko.observable(false); + this.isSparkEnabled.subscribe((isEnabled: boolean) => this.refreshCommandBarButtons()); + this.resourceTree = new ResourceTreeAdapter(this); + this.resourceTreeForResourceToken = new ResourceTreeAdapterForResourceToken(this); + this.notebookServerInfo = ko.observable({ + notebookServerEndpoint: undefined, + authToken: undefined + }); + this.notebookBasePath = ko.observable(Constants.Notebook.defaultBasePath); + this.sparkClusterConnectionInfo = ko.observable({ + userName: undefined, + password: undefined, + endpoints: [] + }); + + // Override notebook server parameters from URL parameters + const featureSubcription = this.features.subscribe(features => { + const serverInfo = this.notebookServerInfo(); + if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) { + serverInfo.notebookServerEndpoint = features[Constants.Features.notebookServerUrl]; + } + + if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) { + serverInfo.authToken = features[Constants.Features.notebookServerToken]; + } + this.notebookServerInfo(serverInfo); + this.notebookServerInfo.valueHasMutated(); + + if (this.isFeatureEnabled(Constants.Features.notebookBasePath)) { + this.notebookBasePath(features[Constants.Features.notebookBasePath]); + } + + if (this.isFeatureEnabled(Constants.Features.livyEndpoint)) { + this.sparkClusterConnectionInfo({ + userName: undefined, + password: undefined, + endpoints: [ + { + endpoint: features[Constants.Features.livyEndpoint], + kind: DataModels.SparkClusterEndpointKind.Livy + } + ] + }); + this.sparkClusterConnectionInfo.valueHasMutated(); + } + + if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) { + updateUserContext({ useSDKOperations: true }); + } + + featureSubcription.dispose(); + }); + + this._dialogProps = ko.observable({ + isModal: false, + visible: false, + title: undefined, + subText: undefined, + primaryButtonText: undefined, + secondaryButtonText: undefined, + onPrimaryButtonClick: undefined, + onSecondaryButtonClick: undefined + }); + this.dialogComponentAdapter = new DialogComponentAdapter(); + this.dialogComponentAdapter.parameters = this._dialogProps; + this.splashScreenAdapter = new SplashScreenComponentAdapter(this); + this.mostRecentActivity = new MostRecentActivity.MostRecentActivity(this); + + this._addSynapseLinkDialogProps = ko.observable({ + isModal: false, + visible: false, + title: undefined, + subText: undefined, + primaryButtonText: undefined, + secondaryButtonText: undefined, + onPrimaryButtonClick: undefined, + onSecondaryButtonClick: undefined + }); + this.addSynapseLinkDialog = new DialogComponentAdapter(); + this.addSynapseLinkDialog.parameters = this._addSynapseLinkDialogProps; + } + + public openEnableSynapseLinkDialog(): void { + const addSynapseLinkDialogProps: DialogProps = { + linkProps: { + linkText: "Learn more", + linkUrl: "https://aka.ms/cosmosdb-synapselink" + }, + isModal: true, + visible: true, + title: `Enable Azure Synapse Link on your Cosmos DB account`, + subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads. + Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`, + primaryButtonText: "Enable Azure Synapse Link", + secondaryButtonText: "Cancel", + + onPrimaryButtonClick: async () => { + const startTime = TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); + const logId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + "Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account." + ); + this.isSynapseLinkUpdating(true); + this._closeSynapseLinkModalDialog(); + + const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id); + + try { + const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync( + this.databaseAccount().id, + "2019-12-12", + { + properties: { + enableAnalyticalStorage: true + } + } + ); + NotificationConsoleUtils.clearInProgressMessageWithId(logId); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Info, + "Enabled Azure Synapse Link for this account" + ); + TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, startTime); + this.databaseAccount(databaseAccount); + } catch (error) { + NotificationConsoleUtils.clearInProgressMessageWithId(logId); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}` + ); + TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, startTime); + } finally { + this.isSynapseLinkUpdating(false); + } + }, + + onSecondaryButtonClick: () => { + this._closeSynapseLinkModalDialog(); + TelemetryProcessor.traceCancel(Action.EnableAzureSynapseLink); + } + }; + this._addSynapseLinkDialogProps(addSynapseLinkDialogProps); + TelemetryProcessor.traceStart(Action.EnableAzureSynapseLink); + + // TODO: return result + } + + public copyUrlLink(src: any, event: MouseEvent): void { + const urlLinkInput: HTMLInputElement = document.getElementById("shareUrlLink") as HTMLInputElement; + urlLinkInput && urlLinkInput.select(); + document.execCommand("copy"); + this.shareUrlCopyHelperText("Copied"); + setTimeout(() => this.shareUrlCopyHelperText("Click to copy"), Constants.ClientDefaults.copyHelperTimeoutMs); + + TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { + description: "Copy full screen URL", + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ShareDialog + }); + } + + public onCopyUrlLinkKeyPress(src: any, event: KeyboardEvent): boolean { + if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { + this.copyUrlLink(src, null); + return false; + } + + return true; + } + + public copyToken(src: any, event: MouseEvent): void { + const tokenInput: HTMLInputElement = document.getElementById("shareToken") as HTMLInputElement; + tokenInput && tokenInput.select(); + document.execCommand("copy"); + this.shareTokenCopyHelperText("Copied"); + setTimeout(() => this.shareTokenCopyHelperText("Click to copy"), Constants.ClientDefaults.copyHelperTimeoutMs); + } + + public onCopyTokenKeyPress(src: any, event: KeyboardEvent): boolean { + if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) { + this.copyToken(src, null); + return false; + } + + return true; + } + + public renewToken = (): void => { + TelemetryProcessor.trace(Action.ConnectEncryptionToken); + this.renewTokenError(""); + const id: string = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + "Initiating connection to account" + ); + this.renewExplorerShareAccess(this, this.tokenForRenewal()) + .fail((error: any) => { + const stringifiedError: string = getErrorMessage(error); + this.renewTokenError("Invalid connection string specified"); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Failed to initiate connection to account: ${stringifiedError}` + ); + }) + .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); + }; + + public generateSharedAccessData(): void { + const id: string = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Generating share url"); + AuthHeadersUtil.generateEncryptedToken().then( + (tokenResponse: DataModels.GenerateTokenResponse) => { + NotificationConsoleUtils.clearInProgressMessageWithId(id); + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully generated share url"); + this.shareAccessData({ + readWriteUrl: this._getShareAccessUrlForToken(tokenResponse.readWrite), + readUrl: this._getShareAccessUrlForToken(tokenResponse.read) + }); + !this.shareAccessData().readWriteUrl && this.shareAccessToggleState(ShareAccessToggleState.Read); // select read toggle by default for readers + this.shareAccessToggleState.valueHasMutated(); // to set initial url and token state + this.shareAccessData.valueHasMutated(); + this._openShareDialog(); + }, + (error: any) => { + NotificationConsoleUtils.clearInProgressMessageWithId(id); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Failed to generate share url: ${getErrorMessage(error)}` + ); + console.error(error); + } + ); + } + + public renewShareAccess(token: string): Q.Promise { + if (!this.renewExplorerShareAccess) { + return Q.reject("Not implemented"); + } + + const deferred: Q.Deferred = Q.defer(); + const id: string = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + "Initiating connection to account" + ); + this.renewExplorerShareAccess(this, token) + .then( + () => { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Connection successful"); + this.renewAdHocAccessPane && this.renewAdHocAccessPane.close(); + deferred.resolve(); + }, + (error: any) => { + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Failed to connect: ${getErrorMessage(error)}` + ); + deferred.reject(error); + } + ) + .finally(() => { + NotificationConsoleUtils.clearInProgressMessageWithId(id); + }); + + return deferred.promise; + } + + public displayGuestAccessTokenRenewalPrompt(): void { + if (!$("#dataAccessTokenModal").dialog("instance")) { + const connectButton = { + text: "Connect", + class: "connectDialogButtons connectButton connectOkBtns", + click: () => { + this.renewAdHocAccessPane.open(); + $("#dataAccessTokenModal").dialog("close"); + } + }; + const cancelButton = { + text: "Cancel", + class: "connectDialogButtons cancelBtn", + click: () => { + $("#dataAccessTokenModal").dialog("close"); + } + }; + + $("#dataAccessTokenModal").dialog({ + autoOpen: false, + buttons: [connectButton, cancelButton], + closeOnEscape: false, + draggable: false, + dialogClass: "no-close", + height: 180, + modal: true, + position: { my: "center center", at: "center center", of: window }, + resizable: false, + title: "Temporary access expired", + width: 435, + close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowDataAccessExpiryDialog(false) + }); + $("#dataAccessTokenModal").dialog("option", "classes", { + "ui-dialog-titlebar": "connectTitlebar" + }); + } + this.shouldShowDataAccessExpiryDialog(true); + $("#dataAccessTokenModal").dialog("open"); + } + + public isConnectExplorerVisible(): boolean { + return $("#connectExplorer").is(":visible") || false; + } + + public displayContextSwitchPromptForConnectionString(connectionString: string): void { + const yesButton = { + text: "OK", + class: "connectDialogButtons okBtn connectOkBtns", + click: () => { + $("#contextSwitchPrompt").dialog("close"); + this.tabsManager.closeTabs(); // clear all tabs so we dont leave any tabs from previous session open + this.renewShareAccess(connectionString); + } + }; + const noButton = { + text: "Cancel", + class: "connectDialogButtons cancelBtn", + click: () => { + $("#contextSwitchPrompt").dialog("close"); + } + }; + + if (!$("#contextSwitchPrompt").dialog("instance")) { + $("#contextSwitchPrompt").dialog({ + autoOpen: false, + buttons: [yesButton, noButton], + closeOnEscape: false, + draggable: false, + dialogClass: "no-close", + height: 255, + modal: true, + position: { my: "center center", at: "center center", of: window }, + resizable: false, + title: "Switch account", + width: 440, + close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowDataAccessExpiryDialog(false) + }); + $("#contextSwitchPrompt").dialog("option", "classes", { + "ui-dialog-titlebar": "connectTitlebar" + }); + $("#contextSwitchPrompt").dialog("option", "open", (event: Event, ui: JQueryUI.DialogUIParams) => { + $(".ui-dialog ").css("z-index", 1001); + $("#contextSwitchPrompt") + .parent() + .siblings(".ui-widget-overlay") + .css("z-index", 1000); + }); + } + $("#contextSwitchPrompt").dialog("option", "buttons", [yesButton, noButton]); // rebind buttons so callbacks accept current connection string + this.shouldShowContextSwitchPrompt(true); + $("#contextSwitchPrompt").dialog("open"); + } + + public displayConnectExplorerForm(): void { + $("#divExplorer").hide(); + $("#connectExplorer").css("display", "flex"); + } + + public hideConnectExplorerForm(): void { + $("#connectExplorer").hide(); + $("#divExplorer").show(); + } + + public isReadWriteToggled: () => boolean = (): boolean => { + return this.shareAccessToggleState() === ShareAccessToggleState.ReadWrite; + }; + + public isReadToggled: () => boolean = (): boolean => { + return this.shareAccessToggleState() === ShareAccessToggleState.Read; + }; + + public toggleReadWrite: (src: any, event: MouseEvent) => void = (src: any, event: MouseEvent) => { + this.shareAccessToggleState(ShareAccessToggleState.ReadWrite); + }; + + public toggleRead: (src: any, event: MouseEvent) => void = (src: any, event: MouseEvent) => { + this.shareAccessToggleState(ShareAccessToggleState.Read); + }; + + public onToggleKeyDown: (src: any, event: KeyboardEvent) => boolean = (src: any, event: KeyboardEvent) => { + if (event.keyCode === Constants.KeyCodes.LeftArrow) { + this.toggleReadWrite(src, null); + return false; + } else if (event.keyCode === Constants.KeyCodes.RightArrow) { + this.toggleRead(src, null); + return false; + } + return true; + }; + + public isDatabaseNodeOrNoneSelected(): boolean { + return this.isNoneSelected() || this.isDatabaseNodeSelected(); + } + + public isDatabaseNodeSelected(): boolean { + return (this.selectedNode() && this.selectedNode().nodeKind === "Database") || false; + } + + public isNodeKindSelected(nodeKind: string): boolean { + return (this.selectedNode() && this.selectedNode().nodeKind === nodeKind) || false; + } + + public isNoneSelected(): boolean { + return this.selectedNode() == null; + } + + public isFeatureEnabled(feature: string): boolean { + const features = this.features(); + + if (!features) { + return false; + } + + if (feature in features && features[feature]) { + return true; + } + + return false; + } + + public logConsoleData(consoleData: ConsoleData): void { + this.notificationConsoleData.splice(0, 0, consoleData); + } + + public deleteInProgressConsoleDataWithId(id: string): void { + const updatedConsoleData = _.reject( + this.notificationConsoleData(), + (data: ConsoleData) => data.type === ConsoleDataType.InProgress && data.id === id + ); + this.notificationConsoleData(updatedConsoleData); + } + + public expandConsole(): void { + this.isNotificationConsoleExpanded(true); + } + + public collapseConsole(): void { + this.isNotificationConsoleExpanded(false); + } + + public toggleLeftPaneExpanded() { + this.isLeftPaneExpanded(!this.isLeftPaneExpanded()); + + if (this.isLeftPaneExpanded()) { + document.getElementById("expandToggleLeftPaneButton").focus(); + this.splitter.expandLeft(); + } else { + document.getElementById("collapseToggleLeftPaneButton").focus(); + this.splitter.collapseLeft(); + } + } + + public refreshDatabaseForResourceToken(): Q.Promise { + const databaseId = this.resourceTokenDatabaseId(); + const collectionId = this.resourceTokenCollectionId(); + if (!databaseId || !collectionId) { + return Q.reject(); + } + + const deferred: Q.Deferred = Q.defer(); + readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => { + this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection)); + this.selectedNode(this.resourceTokenCollection()); + deferred.resolve(); + }); + + return deferred.promise; + } + + public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise { + this.isRefreshingExplorer(true); + const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree + }); + let resourceTreeStartKey: number = null; + if (isInitialLoad) { + resourceTreeStartKey = TelemetryProcessor.traceStart(Action.LoadResourceTree, { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree + }); + } + + // TODO: Refactor + const deferred: Q.Deferred = Q.defer(); + this._setLoadingStatusText("Fetching databases..."); + readDatabases().then( + (databases: DataModels.Database[]) => { + this._setLoadingStatusText("Successfully fetched databases."); + TelemetryProcessor.traceSuccess( + Action.LoadDatabases, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree + }, + startKey + ); + const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode(); + const deltaDatabases = this.getDeltaDatabases(databases); + this.addDatabasesToList(deltaDatabases.toAdd); + this.deleteDatabasesFromList(deltaDatabases.toDelete); + this.selectedNode(currentlySelectedNode); + this._setLoadingStatusText("Fetching containers..."); + this.refreshAndExpandNewDatabases(deltaDatabases.toAdd) + .then( + () => { + this._setLoadingStatusText("Successfully fetched containers."); + deferred.resolve(); + }, + reason => { + this._setLoadingStatusText("Failed to fetch containers."); + deferred.reject(reason); + } + ) + .finally(() => this.isRefreshingExplorer(false)); + }, + error => { + this._setLoadingStatusText("Failed to fetch databases."); + this.isRefreshingExplorer(false); + deferred.reject(error); + const errorMessage = getErrorMessage(error); + TelemetryProcessor.traceFailure( + Action.LoadDatabases, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + error: errorMessage, + errorStack: getErrorStack(error) + }, + startKey + ); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Error while refreshing databases: ${errorMessage}` + ); + } + ); + + return deferred.promise.then( + () => { + if (resourceTreeStartKey != null) { + TelemetryProcessor.traceSuccess( + Action.LoadResourceTree, + { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree + }, + resourceTreeStartKey + ); + } + }, + error => { + if (resourceTreeStartKey != null) { + TelemetryProcessor.traceFailure( + Action.LoadResourceTree, + { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + error: getErrorMessage(error), + errorStack: getErrorStack(error) + }, + resourceTreeStartKey + ); + } + } + ); + } + + public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { + this.onRefreshResourcesClick(source, null); + return false; + } + return true; + }; + + public onRefreshResourcesClick = (source: any, event: MouseEvent): void => { + const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, { + description: "Refresh button clicked", + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree + }); + this.isRefreshingExplorer(true); + this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); + this.refreshNotebookList(); + }; + + public toggleLeftPaneExpandedKeyPress = (source: any, event: KeyboardEvent): boolean => { + if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { + this.toggleLeftPaneExpanded(); + return false; + } + return true; + }; + + // Facade + public provideFeedbackEmail = () => { + window.open(Constants.Urls.feedbackEmail, "_self"); + }; + + public async getArcadiaToken(): Promise { + return new Promise((resolve: (token: string) => void, reject: (error: any) => void) => { + sendCachedDataMessage(MessageTypes.GetArcadiaToken, undefined /** params **/).then( + (token: string) => { + resolve(token); + }, + (error: any) => { + Logger.logError(getErrorMessage(error), "Explorer/getArcadiaToken"); + resolve(undefined); + } + ); + }); + } + + private async _getArcadiaWorkspaces(): Promise { + try { + const workspaces = await this._arcadiaManager.listWorkspacesAsync([userContext.subscriptionId]); + let workspaceItems: ArcadiaWorkspaceItem[] = new Array(workspaces.length); + const sparkPromises: Promise[] = []; + workspaces.forEach((workspace, i) => { + let promise = this._arcadiaManager.listSparkPoolsAsync(workspaces[i].id).then( + sparkpools => { + workspaceItems[i] = { ...workspace, sparkPools: sparkpools }; + }, + error => { + Logger.logError(getErrorMessage(error), "Explorer/this._arcadiaManager.listSparkPoolsAsync"); + } + ); + sparkPromises.push(promise); + }); + + return Promise.all(sparkPromises).then(() => workspaceItems); + } catch (error) { + handleError(error, "Explorer/this._arcadiaManager.listWorkspacesAsync", "Get Arcadia workspaces failed"); + return Promise.resolve([]); + } + } + + public async createWorkspace(): Promise { + return sendCachedDataMessage(MessageTypes.CreateWorkspace, undefined /** params **/); + } + + public async createSparkPool(workspaceId: string): Promise { + return sendCachedDataMessage(MessageTypes.CreateSparkPool, [workspaceId]); + } + + public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise { + if (!databaseAccount) { + throw new Error("No database account specified"); + } + + if (this._isInitializingNotebooks) { + return; + } + this._isInitializingNotebooks = true; + + await this.ensureNotebookWorkspaceRunning(); + let connectionInfo: DataModels.NotebookWorkspaceConnectionInfo = { + authToken: undefined, + notebookServerEndpoint: undefined + }; + try { + connectionInfo = await this.notebookWorkspaceManager.getNotebookConnectionInfoAsync( + databaseAccount.id, + "default" + ); + } catch (error) { + this._isInitializingNotebooks = false; + handleError( + error, + "initNotebooks/getNotebookConnectionInfoAsync", + `Failed to get notebook workspace connection info: ${getErrorMessage(error)}` + ); + throw error; + } finally { + // Overwrite with feature flags + if (this.isFeatureEnabled(Constants.Features.notebookServerUrl)) { + connectionInfo.notebookServerEndpoint = this.features()[Constants.Features.notebookServerUrl]; + } + + if (this.isFeatureEnabled(Constants.Features.notebookServerToken)) { + connectionInfo.authToken = this.features()[Constants.Features.notebookServerToken]; + } + + this.notebookServerInfo(connectionInfo); + this.notebookServerInfo.valueHasMutated(); + this.refreshNotebookList(); + } + + this._isInitializingNotebooks = false; + } + + public resetNotebookWorkspace() { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) { + handleError( + "Attempt to reset notebook workspace, but notebook is not enabled", + "Explorer/resetNotebookWorkspace" + ); + return; + } + const resetConfirmationDialogProps: DialogProps = { + isModal: true, + visible: true, + title: "Reset Workspace", + subText: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?", + primaryButtonText: "OK", + secondaryButtonText: "Cancel", + onPrimaryButtonClick: this._resetNotebookWorkspace, + onSecondaryButtonClick: this._closeModalDialog + }; + this._dialogProps(resetConfirmationDialogProps); + } + + private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise { + if (!databaseAccount) { + return false; + } + + try { + const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id); + return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default"); + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); + return false; + } + } + + private async ensureNotebookWorkspaceRunning() { + if (!this.databaseAccount()) { + return; + } + + let clearMessage; + try { + const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync( + this.databaseAccount().id, + "default" + ); + if ( + notebookWorkspace && + notebookWorkspace.properties && + notebookWorkspace.properties.status && + notebookWorkspace.properties.status.toLowerCase() === "stopped" + ) { + clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace"); + await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default"); + } + } catch (error) { + handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace"); + } finally { + clearMessage && clearMessage(); + } + } + + private _resetNotebookWorkspace = async () => { + this._closeModalDialog(); + const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Resetting notebook workspace"); + try { + await this.notebookManager?.notebookClient.resetWorkspace(); + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully reset notebook workspace"); + TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace); + } catch (error) { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`); + TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, { + error: getErrorMessage(error), + errorStack: getErrorStack(error) + }); + throw error; + } finally { + NotificationConsoleUtils.clearInProgressMessageWithId(id); + } + }; + + private _closeModalDialog = () => { + this._dialogProps().visible = false; + this._dialogProps.valueHasMutated(); + }; + + private _closeSynapseLinkModalDialog = () => { + this._addSynapseLinkDialogProps().visible = false; + this._addSynapseLinkDialogProps.valueHasMutated(); + }; + + private _shouldProcessMessage(event: MessageEvent): boolean { + if (typeof event.data !== "object") { + return false; + } + if (event.data["signature"] !== "pcIframe") { + return false; + } + if (!("data" in event.data)) { + return false; + } + if (typeof event.data["data"] !== "object") { + return false; + } + + // before initialization completed give exception + const message = event.data.data; + if (!this._importExplorerConfigComplete && message && message.type) { + const messageType = message.type; + switch (messageType) { + case MessageTypes.SendNotification: + case MessageTypes.ClearNotification: + case MessageTypes.LoadingStatus: + case MessageTypes.InitTestExplorer: + return true; + } + } + if (!("inputs" in event.data["data"]) && !this._importExplorerConfigComplete) { + return false; + } + return true; + } + + public handleMessage(event: MessageEvent) { + if (isInvalidParentFrameOrigin(event)) { + return; + } + + if (!this._shouldProcessMessage(event)) { + return; + } + + const message: any = event.data.data; + const inputs: ViewModels.DataExplorerInputsFrame = message.inputs; + + const isRunningInPortal = configContext.platform === Platform.Portal; + const isRunningInDevMode = process.env.NODE_ENV === "development"; + if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) { + inputs.extensionEndpoint = configContext.PROXY_PATH; + } + + this.initDataExplorerWithFrameInputs(inputs); + + const openAction: ActionContracts.DataExplorerAction = message.openAction; + if (!!openAction) { + if (this.isRefreshingExplorer()) { + const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => { + handleOpenAction(openAction, this.nonSystemDatabases(), this); + subscription.dispose(); + }); + } else { + handleOpenAction(openAction, this.nonSystemDatabases(), this); + } + } + if (message.actionType === ActionContracts.ActionType.TransmitCachedData) { + handleCachedDataMessage(message); + return; + } + if (message.type) { + switch (message.type) { + case MessageTypes.UpdateLocationHash: + if (!message.locationHash) { + break; + } + hasher.replaceHash(message.locationHash); + RouteHandler.getInstance().parseHash(message.locationHash); + break; + case MessageTypes.SendNotification: + if (!message.message) { + break; + } + NotificationConsoleUtils.logConsoleMessage( + message.consoleDataType || ConsoleDataType.Info, + message.message, + message.id + ); + break; + case MessageTypes.ClearNotification: + if (!message.id) { + break; + } + NotificationConsoleUtils.clearInProgressMessageWithId(message.id); + break; + case MessageTypes.LoadingStatus: + if (!message.text) { + break; + } + this._setLoadingStatusText(message.text, message.title); + break; + } + return; + } + + this.splashScreenAdapter.forceRender(); + } + + public findSelectedDatabase(): ViewModels.Database { + if (!this.selectedNode()) { + return null; + } + if (this.selectedNode().nodeKind === "Database") { + return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id()); + } + return this.findSelectedCollection().database; + } + + public findDatabaseWithId(databaseId: string): ViewModels.Database { + return _.find(this.databases(), (database: ViewModels.Database) => database.id() === databaseId); + } + + public isLastNonEmptyDatabase(): boolean { + if (this.isLastDatabase() && this.databases()[0].collections && this.databases()[0].collections().length > 0) { + return true; + } + return false; + } + + public isLastDatabase(): boolean { + if (this.databases().length > 1) { + return false; + } + return true; + } + + public isSelectedDatabaseShared(): boolean { + const database = this.findSelectedDatabase(); + if (!!database) { + return database.offer && !!database.offer(); + } + + return false; + } + + public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void { + if (inputs != null) { + // In development mode, save the iframe message from the portal in session storage. + // This allows webpack hot reload to funciton properly + if (process.env.NODE_ENV === "development") { + sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); + } + + const authorizationToken = inputs.authorizationToken || ""; + const masterKey = inputs.masterKey || ""; + const databaseAccount = inputs.databaseAccount || null; + if (inputs.defaultCollectionThroughput) { + this.collectionCreationDefaults = inputs.defaultCollectionThroughput; + } + this.features(inputs.features); + this.serverId(inputs.serverId); + this.databaseAccount(databaseAccount); + this.subscriptionType(inputs.subscriptionType); + this.hasWriteAccess(inputs.hasWriteAccess); + this.flight(inputs.addCollectionDefaultFlight); + this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription); + this.isAuthWithResourceToken(inputs.isAuthWithresourceToken); + this.setFeatureFlagsFromFlights(inputs.flights); + this._importExplorerConfigComplete = true; + + updateConfigContext({ + BACKEND_ENDPOINT: inputs.extensionEndpoint || "", + ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT) + }); + + updateUserContext({ + authorizationToken, + masterKey, + databaseAccount, + resourceGroup: inputs.resourceGroup, + subscriptionId: inputs.subscriptionId, + subscriptionType: inputs.subscriptionType, + quotaId: inputs.quotaId + }); + TelemetryProcessor.traceSuccess( + Action.LoadDatabaseAccount, + { + resourceId: this.databaseAccount && this.databaseAccount().id, + dataExplorerArea: Constants.Areas.ResourceTree, + databaseAccount: this.databaseAccount && this.databaseAccount() + }, + inputs.loadDatabaseAccountTimestamp + ); + + this.isAccountReady(true); + } + } + + public setFeatureFlagsFromFlights(flights: readonly string[]): void { + if (!flights) { + return; + } + if (flights.indexOf(Constants.Flights.AutoscaleTest) !== -1) { + this.isAutoscaleDefaultEnabled(true); + } + if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) { + this.isMongoIndexingEnabled(true); + } + } + + public findSelectedCollection(): ViewModels.Collection { + return (this.selectedNode().nodeKind === "Collection" + ? this.selectedNode() + : this.selectedNode().collection) as ViewModels.Collection; + } + + // TODO: Refactor below methods, minimize dependencies and add unit tests where necessary + public findSelectedStoredProcedure(): StoredProcedure { + const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); + return _.find(selectedCollection.storedProcedures(), (storedProcedure: StoredProcedure) => { + const openedSprocTab = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.StoredProcedures, + tab => tab.node && tab.node.rid === storedProcedure.rid + ); + return ( + storedProcedure.rid === this.selectedNode().rid || + (!!openedSprocTab && openedSprocTab.length > 0 && openedSprocTab[0].isActive()) + ); + }); + } + + public findSelectedUDF(): UserDefinedFunction { + const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); + return _.find(selectedCollection.userDefinedFunctions(), (userDefinedFunction: UserDefinedFunction) => { + const openedUdfTab = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.UserDefinedFunctions, + tab => tab.node && tab.node.rid === userDefinedFunction.rid + ); + return ( + userDefinedFunction.rid === this.selectedNode().rid || + (!!openedUdfTab && openedUdfTab.length > 0 && openedUdfTab[0].isActive()) + ); + }); + } + + public findSelectedTrigger(): Trigger { + const selectedCollection: ViewModels.Collection = this.findSelectedCollection(); + return _.find(selectedCollection.triggers(), (trigger: Trigger) => { + const openedTriggerTab = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.Triggers, + tab => tab.node && tab.node.rid === trigger.rid + ); + return ( + trigger.rid === this.selectedNode().rid || + (!!openedTriggerTab && openedTriggerTab.length > 0 && openedTriggerTab[0].isActive()) + ); + }); + } + + public closeAllPanes(): void { + this._panes.forEach((pane: ContextualPaneBase) => pane.close()); + } + + public isRunningOnNationalCloud(): boolean { + return ( + this.serverId() === Constants.ServerIds.blackforest || + this.serverId() === Constants.ServerIds.fairfax || + this.serverId() === Constants.ServerIds.mooncake + ); + } + + public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void { + this.commandBarComponentAdapter.onUpdateTabsButtons(buttons); + } + + public signInAad = () => { + TelemetryProcessor.trace(Action.SignInAad, undefined, { area: "Explorer" }); + sendMessage({ + type: MessageTypes.AadSignIn + }); + }; + + public onSwitchToConnectionString = () => { + $("#connectWithAad").hide(); + $("#connectWithConnectionString").show(); + }; + + public clickHostedAccountSwitch = () => { + sendMessage({ + type: MessageTypes.UpdateAccountSwitch, + click: true + }); + }; + + public clickHostedDirectorySwitch = () => { + sendMessage({ + type: MessageTypes.UpdateDirectoryControl, + click: true + }); + }; + + public refreshDatabaseAccount = () => { + sendMessage({ + type: MessageTypes.RefreshDatabaseAccount + }); + }; + + private refreshAndExpandNewDatabases(newDatabases: ViewModels.Database[]): Q.Promise { + // we reload collections for all databases so the resource tree reflects any collection-level changes + // i.e addition of stored procedures, etc. + const deferred: Q.Deferred = Q.defer(); + let loadCollectionPromises: Q.Promise[] = []; + + // If the user has a lot of databases, only load expanded databases. + const databasesToLoad = + this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand + ? this.databases() + : this.databases().filter(db => db.isDatabaseExpanded()); + + const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree + }); + databasesToLoad.forEach(async (database: ViewModels.Database) => { + await database.loadCollections(); + const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id()); + if (isNewDatabase) { + database.expandDatabase(); + } + this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().id() === database.id()); + }); + + Q.all(loadCollectionPromises).done( + () => { + deferred.resolve(); + TelemetryProcessor.traceSuccess( + Action.LoadCollections, + { dataExplorerArea: Constants.Areas.ResourceTree }, + startKey + ); + }, + (error: any) => { + deferred.reject(error); + TelemetryProcessor.traceFailure( + Action.LoadCollections, + { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ResourceTree, + error: getErrorMessage(error), + errorStack: getErrorStack(error) + }, + startKey + ); + } + ); + return deferred.promise; + } + + // TODO: Abstract this elsewhere + private _openShareDialog: () => void = (): void => { + if (!$("#shareDataAccessFlyout").dialog("instance")) { + const accountMetadataInfo = { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.ShareDialog + }; + const openFullscreenButton = { + text: "Open", + class: "openFullScreenBtn openFullScreenCancelBtn", + click: () => { + TelemetryProcessor.trace( + Action.SelectItem, + ActionModifiers.Mark, + _.extend({}, { description: "Open full screen" }, accountMetadataInfo) + ); + + const hiddenAnchorElement: HTMLAnchorElement = document.createElement("a"); + hiddenAnchorElement.href = this.shareAccessUrl(); + hiddenAnchorElement.target = "_blank"; + $("#shareDataAccessFlyout").dialog("close"); + hiddenAnchorElement.click(); + } + }; + const cancelButton = { + text: "Cancel", + class: "shareCancelButton openFullScreenCancelBtn", + click: () => { + TelemetryProcessor.trace( + Action.SelectItem, + ActionModifiers.Mark, + _.extend({}, { description: "Cancel open full screen" }, accountMetadataInfo) + ); + $("#shareDataAccessFlyout").dialog("close"); + } + }; + $("#shareDataAccessFlyout").dialog({ + autoOpen: false, + buttons: [openFullscreenButton, cancelButton], + closeOnEscape: true, + draggable: false, + dialogClass: "no-close", + position: { my: "right top", at: "right bottom", of: $(".OpenFullScreen") }, + resizable: false, + title: "Open Full Screen", + width: 400, + close: (event: Event, ui: JQueryUI.DialogUIParams) => this.shouldShowShareDialogContents(false) + }); + $("#shareDataAccessFlyout").dialog("option", "classes", { + "ui-widget-content": "shareUrlDialog", + "ui-widget-header": "shareUrlTitle", + "ui-dialog-titlebar-close": "shareClose", + "ui-button": "shareCloseIcon", + "ui-button-icon": "cancelIcon", + "ui-icon": "" + }); + $("#shareDataAccessFlyout").dialog("option", "open", (event: Event, ui: JQueryUI.DialogUIParams) => + $(".openFullScreenBtn").focus() + ); + } + $("#shareDataAccessFlyout").dialog("close"); + this.shouldShowShareDialogContents(true); + $("#shareDataAccessFlyout").dialog("open"); + }; + + private _getShareAccessUrlForToken(token: string): string { + if (!token) { + return undefined; + } + + const urlPrefixWithKeyParam: string = `${configContext.hostedExplorerURL}?key=`; + const currentActiveTab = this.tabsManager.activeTab(); + + return `${urlPrefixWithKeyParam}${token}#/${(currentActiveTab && currentActiveTab.hashLocation()) || ""}`; + } + + private _initSettings() { + if (!ExplorerSettings.hasSettingsDefined()) { + ExplorerSettings.createDefaultSettings(); + } + } + + public findCollection(databaseId: string, collectionId: string): ViewModels.Collection { + const database: ViewModels.Database = this.databases().find( + (database: ViewModels.Database) => database.id() === databaseId + ); + return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId); + } + + public isLastCollection(): boolean { + let collectionCount = 0; + if (this.databases().length == 0) { + return false; + } + for (let i = 0; i < this.databases().length; i++) { + const database = this.databases()[i]; + collectionCount += database.collections().length; + if (collectionCount > 1) { + return false; + } + } + return true; + } + + private getDeltaDatabases( + updatedDatabaseList: DataModels.Database[] + ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } { + const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { + const databaseExists = _.some( + this.databases(), + (existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id + ); + return !databaseExists; + }); + const databasesToAdd: ViewModels.Database[] = newDatabases.map( + (newDatabase: DataModels.Database) => new Database(this, newDatabase) + ); + + let databasesToDelete: ViewModels.Database[] = []; + ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { + const databasePresentInUpdatedList = _.some( + updatedDatabaseList, + (db: DataModels.Database) => db.id === database.id() + ); + if (!databasePresentInUpdatedList) { + databasesToDelete.push(database); + } + }); + + return { toAdd: databasesToAdd, toDelete: databasesToDelete }; + } + + private addDatabasesToList(databases: ViewModels.Database[]): void { + this.databases( + this.databases() + .concat(databases) + .sort((database1, database2) => database1.id().localeCompare(database2.id())) + ); + } + + private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void { + const databasesToKeep: ViewModels.Database[] = []; + + ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { + const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id); + if (!shouldRemoveDatabase) { + databasesToKeep.push(database); + } + }); + + this.databases(databasesToKeep); + } + + public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to upload notebook, but notebook is not enabled"; + handleError(error, "Explorer/uploadFile"); + throw new Error(error); + } + + const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); + promise + .then(() => this.resourceTree.triggerRender()) + .catch((reason: any) => this.showOkModalDialog("Unable to upload file", reason)); + return promise; + } + + public async importAndOpen(path: string): Promise { + const name = NotebookUtil.getName(path); + const item = NotebookUtil.createNotebookContentItem(name, path, "file"); + const parent = this.resourceTree.myNotebooksContentRoot; + + if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { + const existingItem = _.find(parent.children, node => node.name === name); + if (existingItem) { + return this.openNotebook(existingItem); + } + + const content = await this.readFile(item); + const uploadedItem = await this.uploadFile(name, content, parent); + return this.openNotebook(uploadedItem); + } + + return Promise.resolve(false); + } + + public async importAndOpenContent(name: string, content: string): Promise { + const parent = this.resourceTree.myNotebooksContentRoot; + + if (parent && parent.children && this.isNotebookEnabled() && this.notebookManager?.notebookClient) { + if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) { + this.notebookToImport = undefined; // we don't want to try opening this notebook again + } + + const existingItem = _.find(parent.children, node => node.name === name); + if (existingItem) { + return this.openNotebook(existingItem); + } + + const uploadedItem = await this.uploadFile(name, content, parent); + return this.openNotebook(uploadedItem); + } + + this.notebookToImport = { name, content }; // we'll try opening this notebook later on + return Promise.resolve(false); + } + + public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise { + if (this.notebookManager) { + await this.notebookManager.openPublishNotebookPane( + name, + content, + parentDomElement, + this.isLinkInjectionEnabled() + ); + this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; + this.isPublishNotebookPaneEnabled(true); + } + } + + public copyNotebook(name: string, content: string): void { + if (this.notebookManager) { + this.notebookManager.openCopyNotebookPane(name, content); + this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter; + this.isCopyNotebookPaneEnabled(true); + } + } + + public showOkModalDialog(title: string, msg: string): void { + this._dialogProps({ + isModal: true, + visible: true, + title, + subText: msg, + primaryButtonText: "Close", + secondaryButtonText: undefined, + onPrimaryButtonClick: this._closeModalDialog, + onSecondaryButtonClick: undefined + }); + } + + public showOkCancelModalDialog( + title: string, + msg: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps, + isPrimaryButtonDisabled?: boolean + ): void { + this._dialogProps({ + isModal: true, + visible: true, + title, + subText: msg, + primaryButtonText: okLabel, + secondaryButtonText: cancelLabel, + onPrimaryButtonClick: () => { + this._closeModalDialog(); + onOk && onOk(); + }, + onSecondaryButtonClick: () => { + this._closeModalDialog(); + onCancel && onCancel(); + }, + choiceGroupProps, + textFieldProps, + primaryButtonDisabled: isPrimaryButtonDisabled + }); + } + + /** + * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. + * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. + * Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder + * will not fetch its content if the children array exists (and has only one child which was manually created). + * Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal. + * + * @param name + * @param path + */ + public createNotebookContentItemFile(name: string, path: string): NotebookContentItem { + return NotebookUtil.createNotebookContentItem(name, path, "file"); + } + + public async openNotebook(notebookContentItem: NotebookContentItem): Promise { + if (!notebookContentItem || !notebookContentItem.path) { + throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); + } + + const notebookTabs = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.NotebookV2, + tab => + (tab as NotebookV2Tab).notebookPath && + FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path) + ) as NotebookV2Tab[]; + let notebookTab = notebookTabs && notebookTabs[0]; + + if (notebookTab) { + this.tabsManager.activateTab(notebookTab); + } else { + const options: NotebookTabOptions = { + account: userContext.databaseAccount, + tabKind: ViewModels.CollectionTabKind.NotebookV2, + node: null, + title: notebookContentItem.name, + tabPath: notebookContentItem.path, + collection: null, + masterKey: userContext.masterKey || "", + hashLocation: "notebooks", + isActive: ko.observable(false), + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: null, + onUpdateTabsButtons: this.onUpdateTabsButtons, + container: this, + notebookContentItem + }; + + try { + const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab"); + notebookTab = new NotebookTabV2.default(options); + this.tabsManager.activateNewTab(notebookTab); + } catch (reason) { + console.error("Import NotebookV2Tab failed!", reason); + return false; + } + } + + return true; + } + + public renameNotebook(notebookFile: NotebookContentItem): Q.Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to rename notebook, but notebook is not enabled"; + handleError(error, "Explorer/renameNotebook"); + throw new Error(error); + } + + // Don't delete if tab is open to avoid accidental deletion + const openedNotebookTabs = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.NotebookV2, + (tab: NotebookV2Tab) => { + return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); + } + ); + if (openedNotebookTabs.length > 0) { + this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); + return Q.reject(); + } + + const originalPath = notebookFile.path; + const result = this.stringInputPane + .openWithOptions({ + errorMessage: "Could not rename notebook", + inProgressMessage: "Renaming notebook to", + successMessage: "Renamed notebook to", + inputLabel: "Enter new notebook name", + paneTitle: "Rename Notebook", + submitButtonLabel: "Rename", + defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"), + onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input) + }) + .then(newNotebookFile => { + const notebookTabs = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.NotebookV2, + (tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath) + ); + notebookTabs.forEach(tab => { + tab.tabTitle(newNotebookFile.name); + tab.tabPath(newNotebookFile.path); + (tab as NotebookV2Tab).notebookPath(newNotebookFile.path); + }); + + return newNotebookFile; + }); + result.then(() => this.resourceTree.triggerRender()); + return result; + } + + public onCreateDirectory(parent: NotebookContentItem): Q.Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to create notebook directory, but notebook is not enabled"; + handleError(error, "Explorer/onCreateDirectory"); + throw new Error(error); + } + + const result = this.stringInputPane.openWithOptions({ + errorMessage: "Could not create directory ", + inProgressMessage: "Creating directory ", + successMessage: "Created directory ", + inputLabel: "Enter new directory name", + paneTitle: "Create new directory", + submitButtonLabel: "Create", + defaultInput: "", + onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input) + }); + result.then(() => this.resourceTree.triggerRender()); + return result; + } + + public readFile(notebookFile: NotebookContentItem): Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to read file, but notebook is not enabled"; + handleError(error, "Explorer/downloadFile"); + throw new Error(error); + } + + return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path); + } + + public downloadFile(notebookFile: NotebookContentItem): Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to download file, but notebook is not enabled"; + handleError(error, "Explorer/downloadFile"); + throw new Error(error); + } + + const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`); + + return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then( + (content: string) => { + const blob = stringToBlob(content, "text/plain"); + if (navigator.msSaveBlob) { + // for IE and Edge + navigator.msSaveBlob(blob, notebookFile.name); + } else { + const downloadLink: HTMLAnchorElement = document.createElement("a"); + const url = URL.createObjectURL(blob); + downloadLink.href = url; + downloadLink.target = "_self"; + downloadLink.download = notebookFile.name; + + // for some reason, FF displays the download prompt only when + // the link is added to the dom so we add and remove it + document.body.appendChild(downloadLink); + downloadLink.click(); + downloadLink.remove(); + } + + clearMessage(); + }, + (error: any) => { + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Could not download notebook ${getErrorMessage(error)}` + ); + + clearMessage(); + } + ); + } + + private async _refreshNotebooksEnabledStateForAccount(): Promise { + const authType = window.authType as AuthType; + if ( + authType === AuthType.EncryptedToken || + authType === AuthType.ResourceToken || + authType === AuthType.MasterKey + ) { + this.isNotebooksEnabledForAccount(false); + return; + } + + const databaseAccount = this.databaseAccount(); + const databaseAccountLocation = databaseAccount && databaseAccount.location.toLowerCase(); + const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`; + const authorizationHeader = getAuthorizationHeader(); + try { + const response = await fetch(disallowedLocationsUri, { + method: "POST", + body: JSON.stringify({ + resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces] + }), + headers: { + [authorizationHeader.header]: authorizationHeader.token, + [Constants.HttpHeaders.contentType]: "application/json" + } + }); + + if (!response.ok) { + throw new Error("Failed to fetch disallowed locations"); + } + + const disallowedLocations: string[] = await response.json(); + if (!disallowedLocations) { + Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount"); + this.isNotebooksEnabledForAccount(true); + return; + } + const isAccountInAllowedLocation = !disallowedLocations.some( + disallowedLocation => disallowedLocation === databaseAccountLocation + ); + this.isNotebooksEnabledForAccount(isAccountInAllowedLocation); + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount"); + this.isNotebooksEnabledForAccount(false); + } + } + + public _refreshSparkEnabledStateForAccount = async (): Promise => { + const subscriptionId = userContext.subscriptionId; + const armEndpoint = configContext.ARM_ENDPOINT; + const authType = window.authType as AuthType; + if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { + // explorer is not aware of the database account yet + this.isSparkEnabledForAccount(false); + return; + } + + const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`; + const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); + try { + const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync( + featureUri, + Constants.ArmApiVersions.armFeatures + ); + const isEnabled = + (sparkNotebooksFeature && + sparkNotebooksFeature.properties && + sparkNotebooksFeature.properties.state === "Registered") || + false; + this.isSparkEnabledForAccount(isEnabled); + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount"); + this.isSparkEnabledForAccount(false); + } + }; + + public _isAfecFeatureRegistered = async (featureName: string): Promise => { + const subscriptionId = userContext.subscriptionId; + const armEndpoint = configContext.ARM_ENDPOINT; + const authType = window.authType as AuthType; + if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { + // explorer is not aware of the database account yet + return false; + } + + const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`; + const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); + try { + const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync( + featureUri, + Constants.ArmApiVersions.armFeatures + ); + const isEnabled = + (featureStatus && featureStatus.properties && featureStatus.properties.state === "Registered") || false; + return isEnabled; + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount"); + return false; + } + }; + private refreshNotebookList = async (): Promise => { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + return; + } + + await this.resourceTree.initialize(); + this.notebookManager?.refreshPinnedRepos(); + if (this.notebookToImport) { + this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); + } + }; + + public deleteNotebookFile(item: NotebookContentItem): Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to delete notebook file, but notebook is not enabled"; + handleError(error, "Explorer/deleteNotebookFile"); + throw new Error(error); + } + + // Don't delete if tab is open to avoid accidental deletion + const openedNotebookTabs = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.NotebookV2, + (tab: NotebookV2Tab) => { + return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); + } + ); + if (openedNotebookTabs.length > 0) { + this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); + return Promise.reject(); + } + + if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) { + this._dialogProps({ + isModal: true, + visible: true, + title: "Unable to delete file", + subText: "Directory is not empty.", + primaryButtonText: "Close", + secondaryButtonText: undefined, + onPrimaryButtonClick: this._closeModalDialog, + onSecondaryButtonClick: undefined + }); + return Promise.reject(); + } + + return this.notebookManager?.notebookContentClient.deleteContentItem(item).then( + () => { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted: ${item.path}`); + }, + (reason: any) => { + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Failed to delete "${item.path}": ${JSON.stringify(reason)}` + ); + } + ); + } + + /** + * This creates a new notebook file, then opens the notebook + */ + public onNewNotebookClicked(parent?: NotebookContentItem): void { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to create new notebook, but notebook is not enabled"; + handleError(error, "Explorer/onNewNotebookClicked"); + throw new Error(error); + } + + parent = parent || this.resourceTree.myNotebooksContentRoot; + + const notificationProgressId = NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.InProgress, + `Creating new notebook in ${parent.path}` + ); + + const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, { + databaseAccountName: this.databaseAccount() && this.databaseAccount().name, + defaultExperience: this.defaultExperience && this.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook + }); + + this.notebookManager?.notebookContentClient + .createNewNotebookFile(parent) + .then((newFile: NotebookContentItem) => { + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully created: ${newFile.name}`); + TelemetryProcessor.traceSuccess( + Action.CreateNewNotebook, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook + }, + startKey + ); + return this.openNotebook(newFile); + }) + .then(() => this.resourceTree.triggerRender()) + .catch((error: any) => { + const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); + TelemetryProcessor.traceFailure( + Action.CreateNewNotebook, + { + databaseAccountName: this.databaseAccount().name, + defaultExperience: this.defaultExperience(), + dataExplorerArea: Constants.Areas.Notebook, + error: errorMessage, + errorStack: getErrorStack(error) + }, + startKey + ); + }) + .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(notificationProgressId)); + } + + public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void { + parent = parent || this.resourceTree.myNotebooksContentRoot; + + this.uploadFilePane.openWithOptions({ + paneTitle: "Upload file to notebook server", + selectFileInputLabel: "Select file to upload", + errorMessage: "Could not upload file", + inProgressMessage: "Uploading file to notebook server", + successMessage: "Successfully uploaded file to notebook server", + onSubmit: async (file: File): Promise => { + const readFileAsText = (inputFile: File): Promise => { + const reader = new FileReader(); + return new Promise((resolve, reject) => { + reader.onerror = () => { + reader.abort(); + reject(`Problem parsing file: ${inputFile}`); + }; + reader.onload = () => { + resolve(reader.result as string); + }; + reader.readAsText(inputFile); + }); + }; + + const fileContent = await readFileAsText(file); + return this.uploadFile(file.name, fileContent, parent); + }, + extensions: undefined, + submitButtonLabel: "Upload" + }); + } + + public refreshContentItem(item: NotebookContentItem): Promise { + if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { + const error = "Attempt to refresh notebook list, but notebook is not enabled"; + handleError(error, "Explorer/refreshContentItem"); + return Promise.reject(new Error(error)); + } + + return this.notebookManager?.notebookContentClient.updateItemChildren(item); + } + + public getNotebookBasePath(): string { + return this.notebookBasePath(); + } + + public openNotebookTerminal(kind: ViewModels.TerminalKind) { + let title: string; + let hashLocation: string; + + switch (kind) { + case ViewModels.TerminalKind.Default: + title = "Terminal"; + hashLocation = "terminal"; + break; + + case ViewModels.TerminalKind.Mongo: + title = "Mongo Shell"; + hashLocation = "mongo-shell"; + break; + + case ViewModels.TerminalKind.Cassandra: + title = "Cassandra Shell"; + hashLocation = "cassandra-shell"; + break; + + default: + throw new Error("Terminal kind: ${kind} not supported"); + } + + const terminalTabs: TerminalTab[] = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.Terminal, + tab => tab.hashLocation() == hashLocation + ) as TerminalTab[]; + let terminalTab: TerminalTab = terminalTabs && terminalTabs[0]; + + if (terminalTab) { + this.tabsManager.activateTab(terminalTab); + } else { + const newTab = new TerminalTab({ + account: userContext.databaseAccount, + tabKind: ViewModels.CollectionTabKind.Terminal, + node: null, + title: title, + tabPath: title, + collection: null, + hashLocation: hashLocation, + isActive: ko.observable(false), + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: null, + onUpdateTabsButtons: this.onUpdateTabsButtons, + container: this, + kind: kind + }); + + this.tabsManager.activateNewTab(newTab); + } + } + + public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) { + let title: string = "Gallery"; + let hashLocation: string = "gallery"; + + const galleryTabs = this.tabsManager.getTabs( + ViewModels.CollectionTabKind.Gallery, + tab => tab.hashLocation() == hashLocation + ); + let galleryTab = galleryTabs && galleryTabs[0]; + + if (galleryTab) { + this.tabsManager.activateTab(galleryTab); + } else { + if (!this.galleryTab) { + this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab"); + } + + const newTab = new this.galleryTab.default({ + // GalleryTabOptions + account: userContext.databaseAccount, + container: this, + junoClient: this.notebookManager?.junoClient, + notebookUrl, + galleryItem, + isFavorite, + // TabOptions + tabKind: ViewModels.CollectionTabKind.Gallery, + title: title, + tabPath: title, + documentClientUtility: null, + isActive: ko.observable(false), + hashLocation: hashLocation, + onUpdateTabsButtons: this.onUpdateTabsButtons, + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: null + }); + + this.tabsManager.activateNewTab(newTab); + } + } + + public async openNotebookViewer(notebookUrl: string) { + const title = path.basename(notebookUrl); + const hashLocation = notebookUrl; + + if (!this.notebookViewerTab) { + this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab"); + } + + const notebookViewerTabModule = this.notebookViewerTab; + + let isNotebookViewerOpen = (tab: TabsBase) => { + const notebookViewerTab = tab as typeof notebookViewerTabModule.default; + return notebookViewerTab.notebookUrl === notebookUrl; + }; + + const notebookViewerTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2, tab => { + return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab); + }); + let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0]; + + if (notebookViewerTab) { + this.tabsManager.activateNewTab(notebookViewerTab); + } else { + notebookViewerTab = new this.notebookViewerTab.default({ + account: userContext.databaseAccount, + tabKind: ViewModels.CollectionTabKind.NotebookViewer, + node: null, + title: title, + tabPath: title, + documentClientUtility: null, + collection: null, + hashLocation: hashLocation, + isActive: ko.observable(false), + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: null, + onUpdateTabsButtons: this.onUpdateTabsButtons, + container: this, + notebookUrl + }); + + this.tabsManager.activateNewTab(notebookViewerTab); + } + } + + public onNewCollectionClicked(): void { + if (this.isPreferredApiCassandra()) { + this.cassandraAddCollectionPane.open(); + } else { + this.addCollectionPane.open(this.selectedDatabaseId()); + } + document.getElementById("linkAddCollection").focus(); + } + + private refreshCommandBarButtons(): void { + const activeTab = this.tabsManager.activeTab(); + if (activeTab) { + activeTab.onActivate(); // TODO only update tabs buttons? + } else { + this.onUpdateTabsButtons([]); + } + } + + private getTokenRefreshInterval(token: string): number { + let tokenRefreshInterval = Constants.ClientDefaults.arcadiaTokenRefreshInterval; + if (!token) { + return tokenRefreshInterval; + } + + try { + const tokenPayload = decryptJWTToken(this.arcadiaToken()); + if (tokenPayload && tokenPayload.hasOwnProperty("exp")) { + const expirationTime = tokenPayload.exp as number; // seconds since unix epoch + const now = new Date().getTime() / 1000; + const tokenExpirationIntervalInMs = (expirationTime - now) * 1000; + if (tokenExpirationIntervalInMs < tokenRefreshInterval) { + tokenRefreshInterval = + tokenExpirationIntervalInMs - Constants.ClientDefaults.arcadiaTokenRefreshIntervalPaddingMs; + } + } + return tokenRefreshInterval; + } catch (error) { + Logger.logError(getErrorMessage(error), "Explorer/getTokenRefreshInterval"); + return tokenRefreshInterval; + } + } + + private _setLoadingStatusText(text: string, title: string = "Welcome to Azure Cosmos DB") { + if (!text) { + return; + } + + const loadingText = document.getElementById("explorerLoadingStatusText"); + if (!loadingText) { + Logger.logError( + "getElementById('explorerLoadingStatusText') failed to find element", + "Explorer/_setLoadingStatusText" + ); + return; + } + loadingText.innerHTML = text; + + const loadingTitle = document.getElementById("explorerLoadingStatusTitle"); + if (!loadingTitle) { + Logger.logError( + "getElementById('explorerLoadingStatusTitle') failed to find element", + "Explorer/_setLoadingStatusText" + ); + } else { + loadingTitle.innerHTML = title; + } + } + + private _openSetupNotebooksPaneForQuickstart(): void { + const title = "Enable Notebooks (Preview)"; + const description = + "You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account."; + + this.setupNotebooksPane.openWithTitleAndDescription(title, description); + } + + public async handleOpenFileAction(path: string): Promise { + if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(this.databaseAccount()))) { + this.closeAllPanes(); + this._openSetupNotebooksPaneForQuickstart(); + } + + // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb + // when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly + // calling GitHub. For now convert this url to a raw url and download content. + const gitHubInfo = fromContentUri(path); + if (gitHubInfo) { + const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path); + const response = await fetch(rawUrl); + if (response.status === Constants.HttpStatusCodes.OK) { + this.notebookToImport = { + name: NotebookUtil.getName(path), + content: await response.text() + }; + + this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content); + } + } + } + + public async loadSelectedDatabaseOffer(): Promise { + const database = this.findSelectedDatabase(); + await database?.loadOffer(); + } + + public async loadDatabaseOffers(): Promise { + await Promise.all( + this.databases()?.map(async (database: ViewModels.Database) => { + await database.loadOffer(); + }) + ); + } + + public isFirstResourceCreated(): boolean { + const databases: ViewModels.Database[] = this.databases(); + + if (!databases || databases.length === 0) { + return false; + } + + return databases.some(database => { + // user has created at least one collection + if (database.collections()?.length > 0) { + return true; + } + // user has created a database with shared throughput + if (database.offer()) { + return true; + } + // use has created an empty database without shared throughput + return false; + }); + } +} diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index 9f705551e..43f4e2deb 100644 --- a/src/Explorer/Panes/AddCollectionPane.ts +++ b/src/Explorer/Panes/AddCollectionPane.ts @@ -929,8 +929,8 @@ export default class AddCollectionPane extends ContextualPaneBase { this.databaseId(""); this.partitionKey(""); this.throughputSpendAck(false); - this.isAutoPilotSelected(false); - this.isSharedAutoPilotSelected(false); + this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); + this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); diff --git a/src/Explorer/Panes/AddDatabasePane.ts b/src/Explorer/Panes/AddDatabasePane.ts index 5119923b0..64d0ad4b8 100644 --- a/src/Explorer/Panes/AddDatabasePane.ts +++ b/src/Explorer/Panes/AddDatabasePane.ts @@ -337,7 +337,7 @@ export default class AddDatabasePane extends ContextualPaneBase { public resetData() { this.databaseId(""); this.databaseCreateNewShared(this.getSharedThroughputDefault()); - this.isAutoPilotSelected(false); + this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput); this._updateThroughputLimitByDatabase(); this.throughputSpendAck(false); diff --git a/src/Explorer/Panes/CassandraAddCollectionPane.ts b/src/Explorer/Panes/CassandraAddCollectionPane.ts index 092f032a7..061f051c7 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane.ts +++ b/src/Explorer/Panes/CassandraAddCollectionPane.ts @@ -451,8 +451,8 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { public resetData() { super.resetData(); const throughputDefaults = this.container.collectionCreationDefaults.throughput; - this.isAutoPilotSelected(false); - this.isSharedAutoPilotSelected(false); + this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); + this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled()); this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.throughput(AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container)); diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index 0a3a0d069..c794fb1b0 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -22,6 +22,7 @@ export enum Action { MongoShell, ContextualPane, ScaleThroughput, + ToggleAutoscaleSetting, SelectItem, Tab, UpdateDocument, @@ -104,7 +105,9 @@ export const ActionModifiers = { Submit: "submit", IndexAll: "index all properties", NoIndex: "no indexing", - Cancel: "cancel" + Cancel: "cancel", + ToggleAutoscaleOn: "autoscale on", + ToggleAutoscaleOff: "autoscale off" } as const; export enum SourceBlade {