From 6afb950aa9614d21e341668547d5c7d9399cd13b Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 29 Nov 2024 16:30:59 +0100 Subject: [PATCH 01/42] first step, added v2 provider and creating account tree items for Mongo RU --- src/extension.ts | 8 +- .../tree/MongoClustersBranchDataProvider.ts | 3 - src/tree/CosmosAccountModel.ts | 14 ++ src/tree/CosmosAccountResourceItemBase.ts | 34 +++++ src/tree/CosmosBranchDataProvider.ts | 139 ++++++++++++++++++ src/tree/mongo/MongoDatabaseAccountModel.ts | 12 ++ .../mongo/MongoDatabaseAccountResourceItem.ts | 19 +++ 7 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 src/tree/CosmosAccountModel.ts create mode 100644 src/tree/CosmosAccountResourceItemBase.ts create mode 100644 src/tree/CosmosBranchDataProvider.ts create mode 100644 src/tree/mongo/MongoDatabaseAccountModel.ts create mode 100644 src/tree/mongo/MongoDatabaseAccountResourceItem.ts diff --git a/src/extension.ts b/src/extension.ts index 3f769b93d..6236c9e62 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -59,6 +59,7 @@ import { DatabaseResolver } from './resolver/AppResolver'; import { DatabaseWorkspaceProvider } from './resolver/DatabaseWorkspaceProvider'; import { TableAccountTreeItem } from './table/tree/TableAccountTreeItem'; import { AttachedAccountSuffix } from './tree/AttachedAccountsTreeItem'; +import { CosmosBranchDataProvider } from './tree/CosmosBranchDataProvider'; import { SubscriptionTreeItem } from './tree/SubscriptionTreeItem'; import { localize } from './utils/localize'; @@ -96,7 +97,12 @@ export async function activateInternal( ext.state = new TreeElementStateManager(); ext.rgApiV2 = await getAzureResourcesExtensionApi(context, '2.0.0'); - ext.rgApi.registerApplicationResourceResolver(AzExtResourceType.AzureCosmosDb, new DatabaseResolver()); + // ext.rgApi.registerApplicationResourceResolver(AzExtResourceType.AzureCosmosDb, new DatabaseResolver()); + ext.rgApiV2.resources.registerAzureResourceBranchDataProvider( + AzExtResourceType.AzureCosmosDb, + new CosmosBranchDataProvider(), + ); + ext.rgApi.registerApplicationResourceResolver( AzExtResourceType.PostgresqlServersStandard, new DatabaseResolver(), diff --git a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts index 2d91a7687..78888f4d9 100644 --- a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts +++ b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { getResourceGroupFromId, uiUtils } from '@microsoft/vscode-azext-azureutils'; import { callWithTelemetryAndErrorHandling, nonNullProp, type IActionContext } from '@microsoft/vscode-azext-utils'; import { @@ -22,8 +21,6 @@ import { MongoClusterResourceItem } from './MongoClusterResourceItem'; export interface TreeElementBase extends ResourceModelBase { getChildren?(): vscode.ProviderResult; getTreeItem(): vscode.TreeItem | Thenable; - - //viewProperties?: ViewPropertiesModel; } export class MongoClustersBranchDataProvider diff --git a/src/tree/CosmosAccountModel.ts b/src/tree/CosmosAccountModel.ts new file mode 100644 index 000000000..0019adca0 --- /dev/null +++ b/src/tree/CosmosAccountModel.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Resource } from '@azure/arm-cosmosdb'; +import { type API } from '../AzureDBExperiences'; + +export interface CosmosAccountModel extends Resource { + id: string; + name: string; + + dbExperience: API; +} diff --git a/src/tree/CosmosAccountResourceItemBase.ts b/src/tree/CosmosAccountResourceItemBase.ts new file mode 100644 index 000000000..e882619c2 --- /dev/null +++ b/src/tree/CosmosAccountResourceItemBase.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { type TreeItem } from 'vscode'; + +import * as vscode from 'vscode'; +import { getExperienceFromApi } from '../AzureDBExperiences'; +import { type CosmosAccountModel } from './CosmosAccountModel'; + +export abstract class CosmosAccountResourceItemBase implements TreeElementBase { + id: string; + + constructor(public cosmosAccount: CosmosAccountModel) { + this.id = cosmosAccount.id ?? ''; + } + + /** + * Returns the tree item representation of the cluster. + * @returns The TreeItem object. + */ + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: `${this.cosmosAccount.dbExperience}.item.account`, + label: this.cosmosAccount.name, + description: `(${getExperienceFromApi(this.cosmosAccount.dbExperience).shortName})`, + //iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg'), // Uncomment if icon is available + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + }; + } +} diff --git a/src/tree/CosmosBranchDataProvider.ts b/src/tree/CosmosBranchDataProvider.ts new file mode 100644 index 000000000..defc7aa85 --- /dev/null +++ b/src/tree/CosmosBranchDataProvider.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { + type AzureResource, + type AzureResourceBranchDataProvider, + type ResourceModelBase, +} from '@microsoft/vscode-azureresources-api'; +import * as vscode from 'vscode'; +import { API } from '../AzureDBExperiences'; +import { ext } from '../extensionVariables'; +import { type MongoDatabaseAccountModel } from './mongo/MongoDatabaseAccountModel'; +import { MongoDatabaseAccountResourceItem } from './mongo/MongoDatabaseAccountResourceItem'; + +const resourceTypes = [ + 'microsoft.documentdb/databaseaccounts', // then, investigate .kind for "MongoDB" + 'microsoft.dbforpostgresql/servers', + 'microsoft.dbforpostgresql/flexibleservers', +]; + +export interface TreeElementBase extends ResourceModelBase { + getChildren?(): vscode.ProviderResult; + getTreeItem(): vscode.TreeItem | Thenable; +} + +export class CosmosBranchDataProvider + extends vscode.Disposable + implements AzureResourceBranchDataProvider +{ + private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter(); + + constructor() { + super(() => { + this.onDidChangeTreeDataEmitter.dispose(); + }); + } + + get onDidChangeTreeData(): vscode.Event { + return this.onDidChangeTreeDataEmitter.event; + } + + async getChildren(element: TreeElementBase): Promise { + /** + * getChildren is called for every element in the tree when expanding, the element being expanded is being passed as an argument + */ + return await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { + //context.telemetry.properties.experience = API.MongoClusters; + context.telemetry.properties.parentContext = (await element.getTreeItem()).contextValue ?? 'unknown'; + + return (await element.getChildren?.())?.map((child) => { + if (child.id) { + return ext.state.wrapItemInStateHandling(child as TreeElementBase & { id: string }, () => + this.refresh(child), + ); + } + return child; + }); + }); + } + + async getResourceItem(element: AzureResource): Promise { + /** + * This function is being called when the resource tree is being built, it is called for every resource element in the tree. + */ + + const resourceItem = await callWithTelemetryAndErrorHandling( + 'resolveResource', + // disabling require-await, the async aspect is in there, but uses the .then pattern + // eslint-disable-next-line @typescript-eslint/require-await + async (context: IActionContext) => { + switch (element.azureResourceType.type.toLowerCase()) { + case resourceTypes[0]: { + if (element.azureResourceType.kinds?.includes('mongodb')) { + context.telemetry.properties.experience = API.MongoDB; + + // 1. extract the basic info from the element (subscription, resource group, etc., provided by Azure Resources) + const accountInfo: MongoDatabaseAccountModel = + element as unknown as MongoDatabaseAccountModel; + accountInfo.dbExperience = API.MongoDB; + + const item = new MongoDatabaseAccountResourceItem(element.subscription, accountInfo); + + return item; + } else { + // TODO: really? explore the other options for 'kind', don't we have table, graphapi etc. in there?? + context.telemetry.properties.experience = API.Core; // TODO: verify whether 'else' is still a good choice here + console.log('CoreAPI/NoSQL'); + } + + // const client = await createCosmosDBClient({ ...context, ...subContext }); + // const databaseAccount = await client.databaseAccounts.get(resourceGroupName, name); + // dbChild = await SubscriptionTreeItem.initCosmosDBChild( + // client, + // databaseAccount, + // nonNullValue(subNode), + // ); + // const experience = tryGetExperience(databaseAccount); + + // return experience?.api === API.MongoDB + // ? new ResolvedMongoAccountResource(dbChild as MongoAccountTreeItem, resource) + // : new ResolvedDocDBAccountResource(dbChild as DocDBAccountTreeItem, resource); + return null; + break; + } + case resourceTypes[1]: + case resourceTypes[2]: { + // const postgresClient = + // resource.type.toLowerCase() === resourceTypes[1] + // ? await createPostgreSQLClient({ ...context, ...subContext }) + // : await createPostgreSQLFlexibleClient({ ...context, ...subContext }); + + // postgresServer = await postgresClient.servers.get(resourceGroupName, name); + // dbChild = await SubscriptionTreeItem.initPostgresChild(postgresServer, nonNullValue(subNode)); + + // return new ResolvedPostgresServerResource(dbChild as PostgresServerTreeItem, resource); + return null; + } + default: + return null; + } + }, + ); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return ext.state.wrapItemInStateHandling(resourceItem!, () => this.refresh(resourceItem as TreeElementBase)); + } + + async getTreeItem(element: TreeElementBase): Promise { + const ti = await element.getTreeItem(); + return ti; + } + + refresh(element?: TreeElementBase): void { + this.onDidChangeTreeDataEmitter.fire(element); + } +} diff --git a/src/tree/mongo/MongoDatabaseAccountModel.ts b/src/tree/mongo/MongoDatabaseAccountModel.ts new file mode 100644 index 000000000..606b59057 --- /dev/null +++ b/src/tree/mongo/MongoDatabaseAccountModel.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosAccountModel } from '../CosmosAccountModel'; + +export interface MongoDatabaseAccountModel extends CosmosAccountModel { + // whaterver needed to be added + connectionString?: string; + isServerless?: boolean; +} diff --git a/src/tree/mongo/MongoDatabaseAccountResourceItem.ts b/src/tree/mongo/MongoDatabaseAccountResourceItem.ts new file mode 100644 index 000000000..17d68e640 --- /dev/null +++ b/src/tree/mongo/MongoDatabaseAccountResourceItem.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; +import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; +import { type MongoDatabaseAccountModel } from './MongoDatabaseAccountModel'; + +export class MongoDatabaseAccountResourceItem extends CosmosAccountResourceItemBase { + constructor( + private readonly subscription: AzureSubscription, + databaseAccount: MongoDatabaseAccountModel, + ) { + super(databaseAccount); + } + + // here, we can add more methods or properties specific to MongoDB +} From c4909ebddd02fdf29625c51225d1ba6048cd01fa Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 2 Dec 2024 10:06:55 +0100 Subject: [PATCH 02/42] added entrypoint for nosql + renamed classes for simplicity --- src/tree/CosmosBranchDataProvider.ts | 22 +++++++++++++------ ...seAccountModel.ts => MongoAccountModel.ts} | 2 +- ...rceItem.ts => MongoAccountResourceItem.ts} | 8 +++---- src/tree/nosql/NoSqlAccountModel.ts | 12 ++++++++++ src/tree/nosql/NoSqlAccountResourceItem.ts | 19 ++++++++++++++++ 5 files changed, 51 insertions(+), 12 deletions(-) rename src/tree/mongo/{MongoDatabaseAccountModel.ts => MongoAccountModel.ts} (87%) rename src/tree/mongo/{MongoDatabaseAccountResourceItem.ts => MongoAccountResourceItem.ts} (73%) create mode 100644 src/tree/nosql/NoSqlAccountModel.ts create mode 100644 src/tree/nosql/NoSqlAccountResourceItem.ts diff --git a/src/tree/CosmosBranchDataProvider.ts b/src/tree/CosmosBranchDataProvider.ts index defc7aa85..881093486 100644 --- a/src/tree/CosmosBranchDataProvider.ts +++ b/src/tree/CosmosBranchDataProvider.ts @@ -12,8 +12,10 @@ import { import * as vscode from 'vscode'; import { API } from '../AzureDBExperiences'; import { ext } from '../extensionVariables'; -import { type MongoDatabaseAccountModel } from './mongo/MongoDatabaseAccountModel'; -import { MongoDatabaseAccountResourceItem } from './mongo/MongoDatabaseAccountResourceItem'; +import { type MongoAccountModel } from './mongo/MongoAccountModel'; +import { MongoAccountResourceItem } from './mongo/MongoAccountResourceItem'; +import { type NoSqlAccountModel } from './nosql/NoSqlAccountModel'; +import { NoSqlAccountResourceItem } from './nosql/NoSqlAccountResourceItem'; const resourceTypes = [ 'microsoft.documentdb/databaseaccounts', // then, investigate .kind for "MongoDB" @@ -77,17 +79,23 @@ export class CosmosBranchDataProvider context.telemetry.properties.experience = API.MongoDB; // 1. extract the basic info from the element (subscription, resource group, etc., provided by Azure Resources) - const accountInfo: MongoDatabaseAccountModel = - element as unknown as MongoDatabaseAccountModel; + const accountInfo: MongoAccountModel = element as unknown as MongoAccountModel; accountInfo.dbExperience = API.MongoDB; - const item = new MongoDatabaseAccountResourceItem(element.subscription, accountInfo); + const item = new MongoAccountResourceItem(element.subscription, accountInfo); return item; } else { - // TODO: really? explore the other options for 'kind', don't we have table, graphapi etc. in there?? + // TODO: just "else"? really? explore the other options for 'kind', don't we have table, graphapi etc. in there?? context.telemetry.properties.experience = API.Core; // TODO: verify whether 'else' is still a good choice here - console.log('CoreAPI/NoSQL'); + + // 1. extract the basic info from the element (subscription, resource group, etc., provided by Azure Resources) + const accountInfo: NoSqlAccountModel = element as unknown as NoSqlAccountModel; + accountInfo.dbExperience = API.Core; + + const item = new NoSqlAccountResourceItem(element.subscription, accountInfo); + + return item; } // const client = await createCosmosDBClient({ ...context, ...subContext }); diff --git a/src/tree/mongo/MongoDatabaseAccountModel.ts b/src/tree/mongo/MongoAccountModel.ts similarity index 87% rename from src/tree/mongo/MongoDatabaseAccountModel.ts rename to src/tree/mongo/MongoAccountModel.ts index 606b59057..048d0c342 100644 --- a/src/tree/mongo/MongoDatabaseAccountModel.ts +++ b/src/tree/mongo/MongoAccountModel.ts @@ -5,7 +5,7 @@ import { type CosmosAccountModel } from '../CosmosAccountModel'; -export interface MongoDatabaseAccountModel extends CosmosAccountModel { +export interface MongoAccountModel extends CosmosAccountModel { // whaterver needed to be added connectionString?: string; isServerless?: boolean; diff --git a/src/tree/mongo/MongoDatabaseAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts similarity index 73% rename from src/tree/mongo/MongoDatabaseAccountResourceItem.ts rename to src/tree/mongo/MongoAccountResourceItem.ts index 17d68e640..924163300 100644 --- a/src/tree/mongo/MongoDatabaseAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -5,14 +5,14 @@ import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; -import { type MongoDatabaseAccountModel } from './MongoDatabaseAccountModel'; +import { type MongoAccountModel } from './MongoAccountModel'; -export class MongoDatabaseAccountResourceItem extends CosmosAccountResourceItemBase { +export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { constructor( private readonly subscription: AzureSubscription, - databaseAccount: MongoDatabaseAccountModel, + account: MongoAccountModel, ) { - super(databaseAccount); + super(account); } // here, we can add more methods or properties specific to MongoDB diff --git a/src/tree/nosql/NoSqlAccountModel.ts b/src/tree/nosql/NoSqlAccountModel.ts new file mode 100644 index 000000000..4669c3136 --- /dev/null +++ b/src/tree/nosql/NoSqlAccountModel.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosAccountModel } from '../CosmosAccountModel'; + +export interface NoSqlAccountModel extends CosmosAccountModel { + // whaterver needed to be added + connectionString?: string; + isServerless?: boolean; +} diff --git a/src/tree/nosql/NoSqlAccountResourceItem.ts b/src/tree/nosql/NoSqlAccountResourceItem.ts new file mode 100644 index 000000000..05e209390 --- /dev/null +++ b/src/tree/nosql/NoSqlAccountResourceItem.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; +import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; +import { type NoSqlAccountModel } from './NoSqlAccountModel'; + +export class NoSqlAccountResourceItem extends CosmosAccountResourceItemBase { + constructor( + private readonly subscription: AzureSubscription, + account: NoSqlAccountModel, + ) { + super(account); + } + + // here, we can add more methods or properties specific to MongoDB +} From ed2a0c757ac6bd94505ab07aced258c200bd9054 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 2 Dec 2024 12:13:53 +0100 Subject: [PATCH 03/42] added database support to monogodb ru on v2 --- .../tree/MongoClusterItemBase.ts | 5 +- src/tree/CosmosAccountModel.ts | 3 + src/tree/CosmosBranchDataProvider.ts | 4 +- src/tree/mongo/DatabaseItem.ts | 62 +++++++++ src/tree/mongo/IDatabaseInfo.ts | 9 ++ src/tree/mongo/MongoAccountResourceItem.ts | 119 +++++++++++++++++- src/tree/nosql/NoSqlAccountResourceItem.ts | 3 +- src/utils/azureClients.ts | 8 ++ 8 files changed, 203 insertions(+), 10 deletions(-) create mode 100644 src/tree/mongo/DatabaseItem.ts create mode 100644 src/tree/mongo/IDatabaseInfo.ts diff --git a/src/mongoClusters/tree/MongoClusterItemBase.ts b/src/mongoClusters/tree/MongoClusterItemBase.ts index 5b2b96681..701123fbc 100644 --- a/src/mongoClusters/tree/MongoClusterItemBase.ts +++ b/src/mongoClusters/tree/MongoClusterItemBase.ts @@ -19,10 +19,7 @@ import { type MongoClusterModel } from './MongoClusterModel'; export abstract class MongoClusterItemBase implements TreeElementBase { id: string; - constructor( - // public readonly subscription: AzureSubscription, - public mongoCluster: MongoClusterModel, - ) { + constructor(public mongoCluster: MongoClusterModel) { this.id = mongoCluster.id ?? ''; } diff --git a/src/tree/CosmosAccountModel.ts b/src/tree/CosmosAccountModel.ts index 0019adca0..fa45ef804 100644 --- a/src/tree/CosmosAccountModel.ts +++ b/src/tree/CosmosAccountModel.ts @@ -11,4 +11,7 @@ export interface CosmosAccountModel extends Resource { name: string; dbExperience: API; + + // introduced new properties + resourceGroup?: string; } diff --git a/src/tree/CosmosBranchDataProvider.ts b/src/tree/CosmosBranchDataProvider.ts index 881093486..f74b4584d 100644 --- a/src/tree/CosmosBranchDataProvider.ts +++ b/src/tree/CosmosBranchDataProvider.ts @@ -82,7 +82,7 @@ export class CosmosBranchDataProvider const accountInfo: MongoAccountModel = element as unknown as MongoAccountModel; accountInfo.dbExperience = API.MongoDB; - const item = new MongoAccountResourceItem(element.subscription, accountInfo); + const item = new MongoAccountResourceItem(accountInfo, element.subscription); return item; } else { @@ -93,7 +93,7 @@ export class CosmosBranchDataProvider const accountInfo: NoSqlAccountModel = element as unknown as NoSqlAccountModel; accountInfo.dbExperience = API.Core; - const item = new NoSqlAccountResourceItem(element.subscription, accountInfo); + const item = new NoSqlAccountResourceItem(accountInfo, element.subscription); return item; } diff --git a/src/tree/mongo/DatabaseItem.ts b/src/tree/mongo/DatabaseItem.ts new file mode 100644 index 000000000..e4c575b14 --- /dev/null +++ b/src/tree/mongo/DatabaseItem.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createGenericElement, type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; +import { type IDatabaseInfo } from './IDatabaseInfo'; +import { type MongoAccountModel } from './MongoAccountModel'; + +export class DatabaseItem { + id: string; + + constructor( + readonly account: MongoAccountModel, + readonly databaseInfo: IDatabaseInfo, + ) { + this.id = `${account.id}/${databaseInfo.name}`; + } + + async getChildren(): Promise { + return [ + createGenericElement({ + contextValue: 'mongoClusters.item.no-collection', + id: `${this.id}/no-databases`, + label: 'Create collection...', + commandId: 'command.mongoClusters.createCollection', + commandArgs: [this], + }), + ]; + } + // const client: MongoClustersClient = await MongoClustersClient.getClient(this.mongoCluster.id); + // const collections = await client.listCollections(this.databaseInfo.name); + + // if (collections.length === 0) { + // // no databases in there: + // return [ + // createGenericElement({ + // contextValue: 'mongoClusters.item.no-collection', + // id: `${this.id}/no-databases`, + // label: 'Create collection...', + // iconPath: new vscode.ThemeIcon('plus'), + // commandId: 'command.mongoClusters.createCollection', + // commandArgs: [this], + // }), + // ]; + // } + + // return collections.map((collection) => { + // return new CollectionItem(this.mongoCluster, this.databaseInfo, collection); + // }); + + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: 'mongoClusters.item.database', + label: this.databaseInfo.name, + iconPath: new ThemeIcon('database'), + collapsibleState: TreeItemCollapsibleState.Collapsed, + }; + } +} diff --git a/src/tree/mongo/IDatabaseInfo.ts b/src/tree/mongo/IDatabaseInfo.ts new file mode 100644 index 000000000..d52a50255 --- /dev/null +++ b/src/tree/mongo/IDatabaseInfo.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface IDatabaseInfo { + name?: string; + empty?: boolean; +} diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index 924163300..d9ffe7d17 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -3,17 +3,130 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { type DatabaseAccountGetResults } from '@azure/arm-cosmosdb'; +import { + appendExtensionUserAgent, + callWithTelemetryAndErrorHandling, + nonNullProp, + parseError, + type IActionContext, + type TreeElementBase, +} from '@microsoft/vscode-azext-utils'; import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; +import { type MongoClient } from 'mongodb'; +import { Links, testDb } from '../../constants'; +import { ext } from '../../extensionVariables'; +import { connectToMongoClient } from '../../mongo/connectToMongoClient'; +import { getDatabaseNameFromConnectionString } from '../../mongo/mongoConnectionStrings'; +import { createCosmosDBManagementClient } from '../../utils/azureClients'; import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; +import { DatabaseItem } from './DatabaseItem'; +import { type IDatabaseInfo } from './IDatabaseInfo'; import { type MongoAccountModel } from './MongoAccountModel'; export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { constructor( - private readonly subscription: AzureSubscription, - account: MongoAccountModel, + protected account: MongoAccountModel, + protected subscription?: AzureSubscription, // optional for the case of a workspace connection + readonly databaseAccount?: DatabaseAccountGetResults, + readonly isEmulator?: boolean, ) { super(account); } - // here, we can add more methods or properties specific to MongoDB + async discoverConnectionString(): Promise { + const result = await callWithTelemetryAndErrorHandling( + 'cosmosDB.mongo.authenticate', + async (context: IActionContext) => { + ext.outputChannel.appendLine( + `Cosmos DB for MongoDB (RU): Attempting to authenticate with "${this.account.name}"...`, + ); + // Create a client to interact with the MongoDB vCore management API and read the cluster details + const managementClient = await createCosmosDBManagementClient( + context, + this.subscription as AzureSubscription, + ); + const connectionStringsInfo = await managementClient.databaseAccounts.listConnectionStrings( + this.account.resourceGroup as string, + this.account.name, + ); + + const connectionString: URL = new URL( + nonNullProp(nonNullProp(connectionStringsInfo, 'connectionStrings')[0], 'connectionString'), + ); + // for any Mongo connectionString, append this query param because the Cosmos Mongo API v3.6 doesn't support retrywrites + // but the newer node.js drivers started breaking this + const searchParam: string = 'retrywrites'; + if (!connectionString.searchParams.has(searchParam)) { + connectionString.searchParams.set(searchParam, 'false'); + } + + const cString = connectionString.toString(); + context.valuesToMask.push(cString); + + return cString; + }, + ); + + return result ?? undefined; + } + + async getChildren(): Promise { + ext.outputChannel.appendLine(`Cosmos DB for MongoDB (RU): Loading details for "${this.cosmosAccount.name}"`); + + let mongoClient: MongoClient | undefined; + try { + let databases: IDatabaseInfo[]; + + if (!this.account.connectionString) { + if (this.subscription) { + const cString = await this.discoverConnectionString(); + this.account.connectionString = cString; + } + if (!this.account.connectionString) { + throw new Error('Missing connection string'); + } + } + + // Azure MongoDB accounts need to have the name passed in for private endpoints + mongoClient = await connectToMongoClient( + this.account.connectionString, + this.databaseAccount ? nonNullProp(this.databaseAccount, 'name') : appendExtensionUserAgent(), + ); + + const databaseInConnectionString = getDatabaseNameFromConnectionString(this.account.connectionString); + if (databaseInConnectionString && !this.isEmulator) { + // emulator violates the connection string format + // If the database is in the connection string, that's all we connect to (we might not even have permissions to list databases) + databases = [ + { + name: databaseInConnectionString, + empty: false, + }, + ]; + } else { + // https://mongodb.github.io/node-mongodb-native/3.1/api/index.html + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const result: { databases: IDatabaseInfo[] } = await mongoClient.db(testDb).admin().listDatabases(); + databases = result.databases; + } + return databases + .filter( + (database: IDatabaseInfo) => + !(database.name && database.name.toLowerCase() === 'admin' && database.empty), + ) // Filter out the 'admin' database if it's empty + .map((database) => new DatabaseItem(this.account, database)); + } catch (error) { + const message = parseError(error).message; + if (this.isEmulator && message.includes('ECONNREFUSED')) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + error.message = `Unable to reach emulator. See ${Links.LocalConnectionDebuggingTips} for debugging tips.\n${message}`; + } + throw error; + } finally { + if (mongoClient) { + void mongoClient.close(); + } + } + } } diff --git a/src/tree/nosql/NoSqlAccountResourceItem.ts b/src/tree/nosql/NoSqlAccountResourceItem.ts index 05e209390..055ba00a7 100644 --- a/src/tree/nosql/NoSqlAccountResourceItem.ts +++ b/src/tree/nosql/NoSqlAccountResourceItem.ts @@ -9,9 +9,10 @@ import { type NoSqlAccountModel } from './NoSqlAccountModel'; export class NoSqlAccountResourceItem extends CosmosAccountResourceItemBase { constructor( - private readonly subscription: AzureSubscription, account: NoSqlAccountModel, + /**private*/ readonly subscription?: AzureSubscription, // optional for the case of a workspace connection | private commented out to keep the compiler happy for now ) { + console.log(subscription ? subscription.subscriptionId : 'No subscription'); super(account); } diff --git a/src/utils/azureClients.ts b/src/utils/azureClients.ts index 8762db87d..26b40803f 100644 --- a/src/utils/azureClients.ts +++ b/src/utils/azureClients.ts @@ -17,6 +17,14 @@ export async function createCosmosDBClient(context: AzExtClientContext): Promise return createAzureClient(context, (await import('@azure/arm-cosmosdb')).CosmosDBManagementClient); } +export async function createCosmosDBManagementClient( + context: IActionContext, + subscription: AzureSubscription, +): Promise { + const subContext = createSubscriptionContext(subscription); + return createAzureClient([context, subContext], (await import('@azure/arm-cosmosdb')).CosmosDBManagementClient); +} + export async function createMongoClustersManagementClient( context: IActionContext, subscription: AzureSubscription, From 67eb3e92e7e0e29c3efb3dc42112453e17b54438 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 13 Dec 2024 15:25:19 +0100 Subject: [PATCH 04/42] wip --- src/AzureDBExperiences.ts | 10 + src/mongo/tree/MongoCollectionTreeItem.ts | 317 +++++++++--------- src/mongo/tree/MongoDatabaseTreeItem.ts | 124 +++---- src/mongoClusters/tree/MongoClusterModel.ts | 6 + .../tree/MongoClustersBranchDataProvider.ts | 2 + .../workspace/MongoDBAccountsWorkspaceItem.ts | 2 + src/tree/mongo/DatabaseItem.ts | 62 ---- src/tree/mongo/IDatabaseInfo.ts | 2 +- src/tree/mongo/MongoAccountModel.ts | 7 +- src/tree/mongo/MongoAccountResourceItem.ts | 82 +++-- 10 files changed, 309 insertions(+), 305 deletions(-) delete mode 100644 src/tree/mongo/DatabaseItem.ts diff --git a/src/AzureDBExperiences.ts b/src/AzureDBExperiences.ts index eed43a810..de2d6e42b 100644 --- a/src/AzureDBExperiences.ts +++ b/src/AzureDBExperiences.ts @@ -72,6 +72,9 @@ export interface Experience { shortName: string; description?: string; + // the string used as a telemetry key for a given experience + telemetryName?: string; + // These properties are what the portal actually looks at to determine the difference between APIs kind?: DBAccountKind; capability?: CapabilityName; @@ -120,9 +123,16 @@ export const MongoExperience: Experience = { api: API.MongoDB, longName: 'Cosmos DB for MongoDB', shortName: 'MongoDB', + telemetryName: 'mongo', kind: DBAccountKind.MongoDB, tag: 'Azure Cosmos DB for MongoDB API', } as const; +export const MongoClustersExprience: Experience = { + api: API.MongoClusters, + longName: 'Cosmos DB for MongoDB (vCore)', + shortName: 'MongoDB (vCore)', + telemetryName: 'mongoClusters' +} as const; export const TableExperience: Experience = { api: API.Table, longName: 'Cosmos DB for Table', diff --git a/src/mongo/tree/MongoCollectionTreeItem.ts b/src/mongo/tree/MongoCollectionTreeItem.ts index 30cb61dd2..c13b0e68e 100644 --- a/src/mongo/tree/MongoCollectionTreeItem.ts +++ b/src/mongo/tree/MongoCollectionTreeItem.ts @@ -6,37 +6,29 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - AzExtParentTreeItem, + createGenericElement, DialogResponses, - type AzExtTreeItem, type IActionContext, - type ICreateChildImplContext, - type TreeItemIconPath, + type TreeElementBase, + type TreeElementWithId, } from '@microsoft/vscode-azext-utils'; import assert from 'assert'; import { EJSON } from 'bson'; -import { omit } from 'lodash'; import { - type AnyBulkWriteOperation, type BulkWriteOptions, - type BulkWriteResult, type Collection, type CountOptions, type DeleteResult, type Filter, - type FindCursor, type InsertManyResult, type InsertOneResult, type Document as MongoDocument, } from 'mongodb'; import * as vscode from 'vscode'; -import { type IEditableTreeItem } from '../../DatabasesFileSystem'; -import { ext } from '../../extensionVariables'; -import { nonNullValue } from '../../utils/nonNull'; -import { getDocumentTreeItemLabel } from '../../utils/vscodeUtils'; -import { getBatchSizeSetting } from '../../utils/workspacUtils'; +import { ThemeIcon, type TreeItem } from 'vscode'; +import { type MongoAccountModel } from '../../tree/mongo/MongoAccountModel'; import { type MongoCommand } from '../MongoCommand'; -import { MongoDocumentTreeItem, type IMongoDocument } from './MongoDocumentTreeItem'; +import { type IDatabaseInfo } from './MongoAccountTreeItem'; // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types type MongoFunction = (...args: (Object | Object[] | undefined)[]) => Thenable; @@ -50,161 +42,172 @@ class FunctionDescriptor { ) {} } -export class MongoCollectionTreeItem extends AzExtParentTreeItem implements IEditableTreeItem { - public static contextValue: string = 'MongoCollection'; - public readonly contextValue: string = MongoCollectionTreeItem.contextValue; - public readonly childTypeLabel: string = 'Document'; - public readonly collection: Collection; - public declare parent: AzExtParentTreeItem; +// export class MongoCollectionTreeItem extends AzExtParentTreeItem implements IEditableTreeItem { +export class MongoCollectionTreeItem implements TreeElementWithId { + // public static contextValue: string = 'MongoCollection'; + // public readonly contextValue: string = MongoCollectionTreeItem.contextValue; + // public readonly childTypeLabel: string = 'Document'; + // public readonly collection: Collection; + // public declare parent: AzExtParentTreeItem; // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types public findArgs?: Object[]; public readonly cTime: number = Date.now(); public mTime: number = Date.now(); - private readonly _query: Filter | undefined; - private readonly _projection: object | undefined; - private _cursor: FindCursor | undefined; - private _hasMoreChildren: boolean = true; - private _batchSize: number = getBatchSizeSetting(); + id: string; // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - constructor(parent: AzExtParentTreeItem, collection: Collection, findArgs?: Object[]) { - super(parent); - this.collection = collection; - this.findArgs = findArgs; - if (findArgs && findArgs.length) { - this._query = findArgs[0]; - this._projection = findArgs.length > 1 ? findArgs[1] : undefined; - } - ext.fileSystem.fireChangedEvent(this); + constructor( + readonly account: MongoAccountModel, + readonly databaseInfo: IDatabaseInfo, + readonly collection: Collection, + ) { + this.id = `${account.id}/${databaseInfo.name}/${collection.collectionName}`; } - public async writeFileContent(context: IActionContext, content: string): Promise { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - const documents: IMongoDocument[] = EJSON.parse(content); - const operations: AnyBulkWriteOperation[] = documents.map((document) => { - return { - replaceOne: { - filter: { _id: document._id }, - replacement: omit(document, '_id'), - upsert: false, - }, - }; - }); - - const result: BulkWriteResult = await this.collection.bulkWrite(operations); - ext.outputChannel.appendLog( - `Successfully updated ${result.modifiedCount} document(s), inserted ${result.insertedCount} document(s)`, - ); - - // The current tree item may have been a temporary one used to execute a scrapbook command. - // We want to refresh children for this one _and_ the actual one in the tree (if it's different) - const nodeInTree: MongoCollectionTreeItem | undefined = await ext.rgApi.appResourceTree.findTreeItem( - this.fullId, - context, - ); - const nodesToRefresh: MongoCollectionTreeItem[] = [this]; - if (nodeInTree && this !== nodeInTree) { - nodesToRefresh.push(nodeInTree); - } - - await Promise.all(nodesToRefresh.map((n) => n.refreshChildren(context, documents))); - - if (nodeInTree && this !== nodeInTree) { - // Don't need to fire a changed event on the item being saved at the moment. Just the node in the tree if it's different - ext.fileSystem.fireChangedEvent(nodeInTree); - } + getChildren?(): TreeElementBase[] { + return [ + createGenericElement({ + contextValue: 'mongo.item.documents', + id: `${this.id}/documents`, + label: 'Documents', + // commandId: 'command.internal.mongoClusters.containerView.open', + commandArgs: [ + { + id: this.id, + // viewTitle: `${this.collectionInfo.name}`, + // // viewTitle: `${this.mongoCluster.name}/${this.databaseInfo.name}/${this.collectionInfo.name}`, // using '/' as a separator to use VSCode's "title compression"(?) feature + + // liveConnectionId: this.mongoCluster.id, + // databaseName: this.databaseInfo.name, + // collectionName: this.collectionInfo.name, + // collectionTreeItem: this, + }, + ], + }), + ]; } - public async getFileContent(context: IActionContext): Promise { - const children = await this.getCachedChildren(context); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - return EJSON.stringify( - children.map((c) => c.document), - undefined, - 2, - ); - } - - public get id(): string { - return this.collection.collectionName; - } - - public get label(): string { - return this.collection.collectionName; - } - - public get iconPath(): TreeItemIconPath { - return new vscode.ThemeIcon('files'); - } - - public get filePath(): string { - return this.label + '-cosmos-collection.json'; - } - - public async refreshImpl(): Promise { - this._batchSize = getBatchSizeSetting(); - ext.fileSystem.fireChangedEvent(this); - } - - public async refreshChildren(context: IActionContext, docs: IMongoDocument[]): Promise { - const documentNodes = await this.getCachedChildren(context); - for (const doc of docs) { - const documentNode = documentNodes.find((node) => node.document._id.toString() === doc._id.toString()); - if (documentNode) { - documentNode.document = doc; - await documentNode.refresh(context); - } - } - } - - public hasMoreChildrenImpl(): boolean { - return this._hasMoreChildren; - } - - public async loadMoreChildrenImpl(clearCache: boolean): Promise { - if (clearCache || this._cursor === undefined) { - if (this._query) { - this._cursor = this.collection.find(this._query).batchSize(this._batchSize); - } else { - this._cursor = this.collection.find().batchSize(this._batchSize); - } - if (this._projection) { - this._cursor = this._cursor.project(this._projection); - } - } - - const documents: IMongoDocument[] = []; - let count: number = 0; - while (count < this._batchSize) { - this._hasMoreChildren = await this._cursor.hasNext(); - if (this._hasMoreChildren) { - documents.push(await this._cursor.next()); - count += 1; - } else { - break; - } - } - this._batchSize *= 2; - - return this.createTreeItemsWithErrorHandling( - documents, - 'invalidMongoDocument', - (doc) => new MongoDocumentTreeItem(this, doc), - getDocumentTreeItemLabel, - ); + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: 'mongo.item.collection', + label: this.collection.collectionName, + iconPath: new ThemeIcon('folder-opened'), + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + }; } - public async createChildImpl(context: ICreateChildImplContext): Promise { - context.showCreatingTreeItem(''); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const result: InsertOneResult = await this.collection.insertOne({}); - const newDocument: IMongoDocument = nonNullValue( - await this.collection.findOne({ _id: result.insertedId }), - 'newDocument', - ); - return new MongoDocumentTreeItem(this, newDocument); - } + // public async writeFileContent(context: IActionContext, content: string): Promise { + // // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + // const documents: IMongoDocument[] = EJSON.parse(content); + // const operations: AnyBulkWriteOperation[] = documents.map((document) => { + // return { + // replaceOne: { + // filter: { _id: document._id }, + // replacement: omit(document, '_id'), + // upsert: false, + // }, + // }; + // }); + + // const result: BulkWriteResult = await this.collection.bulkWrite(operations); + // ext.outputChannel.appendLog( + // `Successfully updated ${result.modifiedCount} document(s), inserted ${result.insertedCount} document(s)`, + // ); + + // // The current tree item may have been a temporary one used to execute a scrapbook command. + // // We want to refresh children for this one _and_ the actual one in the tree (if it's different) + // const nodeInTree: MongoCollectionTreeItem | undefined = await ext.rgApi.appResourceTree.findTreeItem( + // this.fullId, + // context, + // ); + // const nodesToRefresh: MongoCollectionTreeItem[] = [this]; + // if (nodeInTree && this !== nodeInTree) { + // nodesToRefresh.push(nodeInTree); + // } + + // await Promise.all(nodesToRefresh.map((n) => n.refreshChildren(context, documents))); + + // if (nodeInTree && this !== nodeInTree) { + // // Don't need to fire a changed event on the item being saved at the moment. Just the node in the tree if it's different + // ext.fileSystem.fireChangedEvent(nodeInTree); + // } + // } + + // public async getFileContent(context: IActionContext): Promise { + // const children = await this.getCachedChildren(context); + // // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + // return EJSON.stringify( + // children.map((c) => c.document), + // undefined, + // 2, + // ); + // } + + // public get filePath(): string { + // return this.label + '-cosmos-collection.json'; + // } + + // public async refreshImpl(): Promise { + // this._batchSize = getBatchSizeSetting(); + // ext.fileSystem.fireChangedEvent(this); + // } + + // public async refreshChildren(context: IActionContext, docs: IMongoDocument[]): Promise { + // const documentNodes = await this.getCachedChildren(context); + // for (const doc of docs) { + // const documentNode = documentNodes.find((node) => node.document._id.toString() === doc._id.toString()); + // if (documentNode) { + // documentNode.document = doc; + // await documentNode.refresh(context); + // } + // } + // } + + // public async loadMoreChildrenImpl(clearCache: boolean): Promise { + // if (clearCache || this._cursor === undefined) { + // if (this._query) { + // this._cursor = this.collection.find(this._query).batchSize(this._batchSize); + // } else { + // this._cursor = this.collection.find().batchSize(this._batchSize); + // } + // if (this._projection) { + // this._cursor = this._cursor.project(this._projection); + // } + // } + + // const documents: IMongoDocument[] = []; + // let count: number = 0; + // while (count < this._batchSize) { + // this._hasMoreChildren = await this._cursor.hasNext(); + // if (this._hasMoreChildren) { + // documents.push(await this._cursor.next()); + // count += 1; + // } else { + // break; + // } + // } + // this._batchSize *= 2; + + // return this.createTreeItemsWithErrorHandling( + // documents, + // 'invalidMongoDocument', + // (doc) => new MongoDocumentTreeItem(this, doc), + // getDocumentTreeItemLabel, + // ); + // } + + // public async createChildImpl(context: ICreateChildImplContext): Promise { + // context.showCreatingTreeItem(''); + // // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + // const result: InsertOneResult = await this.collection.insertOne({}); + // const newDocument: IMongoDocument = nonNullValue( + // await this.collection.findOne({ _id: result.insertedId }), + // 'newDocument', + // ); + // return new MongoDocumentTreeItem(this, newDocument); + // } public async tryExecuteCommandDirectly( command: Partial, @@ -254,7 +257,7 @@ export class MongoCollectionTreeItem extends AzExtParentTreeItem implements IEdi } public async deleteTreeItemImpl(context: IActionContext): Promise { - const message: string = `Are you sure you want to delete collection '${this.label}'?`; + const message: string = `Are you sure you want to delete collection '${this.collection.collectionName}'?`; await context.ui.showWarningMessage( message, { modal: true, stepName: 'deleteMongoCollection' }, diff --git a/src/mongo/tree/MongoDatabaseTreeItem.ts b/src/mongo/tree/MongoDatabaseTreeItem.ts index a36df8026..8ebaf42b6 100644 --- a/src/mongo/tree/MongoDatabaseTreeItem.ts +++ b/src/mongo/tree/MongoDatabaseTreeItem.ts @@ -5,19 +5,19 @@ import { appendExtensionUserAgent, - AzExtParentTreeItem, - DialogResponses, UserCancelledError, type IActionContext, - type ICreateChildImplContext, - type TreeItemIconPath, + type TreeElementBase, } from '@microsoft/vscode-azext-utils'; import * as fse from 'fs-extra'; import { type Collection, type CreateCollectionOptions, type Db } from 'mongodb'; import * as path from 'path'; import * as process from 'process'; import * as vscode from 'vscode'; +import { type TreeItem } from 'vscode'; import { ext } from '../../extensionVariables'; +import { type IDatabaseInfo } from '../../tree/mongo/IDatabaseInfo'; +import { type MongoAccountModel } from '../../tree/mongo/MongoAccountModel'; import * as cpUtils from '../../utils/cp'; import { nonNullProp, nonNullValue } from '../../utils/nonNull'; import { connectToMongoClient } from '../connectToMongoClient'; @@ -31,79 +31,81 @@ import { MongoCollectionTreeItem } from './MongoCollectionTreeItem'; const mongoExecutableFileName = process.platform === 'win32' ? 'mongo.exe' : 'mongo'; const executingInShellMsg = 'Executing command in Mongo shell'; -export class MongoDatabaseTreeItem extends AzExtParentTreeItem { - public static contextValue: string = 'mongoDb'; - public readonly contextValue: string = MongoDatabaseTreeItem.contextValue; - public readonly childTypeLabel: string = 'Collection'; +export class MongoDatabaseTreeItem implements TreeElementBase { + // public static contextValue: string = 'mongoDb'; + // public readonly contextValue: string = MongoDatabaseTreeItem.contextValue; + // public readonly childTypeLabel: string = 'Collection'; public readonly connectionString: string; - public readonly databaseName: string; public declare readonly parent: MongoAccountTreeItem; private _previousShellPathSetting: string | undefined; private _cachedShellPathOrCmd: string | undefined; - constructor(parent: MongoAccountTreeItem, databaseName: string, connectionString: string) { - super(parent); - this.databaseName = databaseName; - this.connectionString = addDatabaseToAccountConnectionString(connectionString, this.databaseName); - } - - public get root(): IMongoTreeRoot { - return this.parent.root; - } + id: string; - public get label(): string { - return this.databaseName; - } - - public get description(): string { - return ext.connectedMongoDB && ext.connectedMongoDB.fullId === this.fullId ? 'Connected' : ''; - } - - public get id(): string { - return this.databaseName; - } + constructor( + readonly account: MongoAccountModel, + readonly databaseInfo: IDatabaseInfo, + ) { + this.id = `${account.id}/${databaseInfo.name}`; - public get iconPath(): TreeItemIconPath { - return new vscode.ThemeIcon('database'); + this.connectionString = addDatabaseToAccountConnectionString(account.connectionString, databaseInfo.name); } - public hasMoreChildrenImpl(): boolean { - return false; - } - - public async loadMoreChildrenImpl(_clearCache: boolean): Promise { + async getChildren(): Promise { const db: Db = await this.connectToDb(); const collections: Collection[] = await db.collections(); - return collections.map((collection) => new MongoCollectionTreeItem(this, collection)); + return collections.map( + (collection) => new MongoCollectionTreeItem(this.account, this.databaseInfo, collection), + ); } - - public async createChildImpl(context: ICreateChildImplContext): Promise { - const collectionName = await context.ui.showInputBox({ - placeHolder: 'Collection Name', - prompt: 'Enter the name of the collection', - stepName: 'createMongoCollection', - validateInput: validateMongoCollectionName, - }); - - context.showCreatingTreeItem(collectionName); - return await this.createCollection(collectionName); + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: 'mongo.item.database', + label: this.databaseInfo.name, + iconPath: new vscode.ThemeIcon('database'), + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + description: ext.connectedMongoDB && ext.connectedMongoDB.id === this.id ? 'Connected' : '', + }; } - public async deleteTreeItemImpl(context: IActionContext): Promise { - const message: string = `Are you sure you want to delete database '${this.label}'?`; - await context.ui.showWarningMessage( - message, - { modal: true, stepName: 'deleteMongoDatabase' }, - DialogResponses.deleteResponse, - ); - const db = await this.connectToDb(); - await db.dropDatabase(); + public get root(): IMongoTreeRoot { + return this.parent.root; } + // public async loadMoreChildrenImpl(_clearCache: boolean): Promise { + // const db: Db = await this.connectToDb(); + // const collections: Collection[] = await db.collections(); + // return collections.map((collection) => new MongoCollectionTreeItem(this, collection)); + // } + + // public async createChildImpl(context: ICreateChildImplContext): Promise { + // const collectionName = await context.ui.showInputBox({ + // placeHolder: 'Collection Name', + // prompt: 'Enter the name of the collection', + // stepName: 'createMongoCollection', + // validateInput: validateMongoCollectionName, + // }); + + // context.showCreatingTreeItem(collectionName); + // return await this.createCollection(collectionName); + // } + + // public async deleteTreeItemImpl(context: IActionContext): Promise { + // const message: string = `Are you sure you want to delete database '${this.databaseInfo.name}'?`; + // await context.ui.showWarningMessage( + // message, + // { modal: true, stepName: 'deleteMongoDatabase' }, + // DialogResponses.deleteResponse, + // ); + // const db = await this.connectToDb(); + // await db.dropDatabase(); + // } + public async connectToDb(): Promise { const accountConnection = await connectToMongoClient(this.connectionString, appendExtensionUserAgent()); - return accountConnection.db(this.databaseName); + return accountConnection.db(this.databaseInfo.name); } public async executeCommand(command: MongoCommand, context: IActionContext): Promise { @@ -111,7 +113,7 @@ export class MongoDatabaseTreeItem extends AzExtParentTreeItem { const db = await this.connectToDb(); const collection = db.collection(command.collection); if (collection) { - const collectionTreeItem = new MongoCollectionTreeItem(this, collection, command.arguments); + const collectionTreeItem = new MongoCollectionTreeItem(this.account, this.databaseInfo, collection); const result = await collectionTreeItem.tryExecuteCommandDirectly(command); if (!result.deferToShell) { return result.result; @@ -145,7 +147,7 @@ export class MongoDatabaseTreeItem extends AzExtParentTreeItem { const result = await newCollection.insertOne({}); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment await newCollection.deleteOne({ _id: result.insertedId }); - return new MongoCollectionTreeItem(this, newCollection); + return new MongoCollectionTreeItem(this.account, this.databaseInfo, newCollection); } private async executeCommandInShell(command: MongoCommand, context: IActionContext): Promise { @@ -161,7 +163,7 @@ export class MongoDatabaseTreeItem extends AzExtParentTreeItem { // requests. const shell = await this.createShell(context); try { - await shell.useDatabase(this.databaseName); + await shell.useDatabase(this.databaseInfo.name); return await shell.executeScript(command.text); } finally { shell.dispose(); diff --git a/src/mongoClusters/tree/MongoClusterModel.ts b/src/mongoClusters/tree/MongoClusterModel.ts index 0ed3993ad..c2acf3906 100644 --- a/src/mongoClusters/tree/MongoClusterModel.ts +++ b/src/mongoClusters/tree/MongoClusterModel.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { type MongoCluster, type Resource } from '@azure/arm-cosmosdb'; +import { type API } from '../../AzureDBExperiences'; // Selecting only the properties used in the extension, but keeping an easy option to extend the model later and offer full coverage of MongoCluster // '|' means that you can only access properties that are common to both types. @@ -32,4 +33,9 @@ interface ResourceModelInUse extends Resource { // introduced new properties resourceGroup?: string; + + // adding support for MongoRU and vCore + dbExperience?: API.MongoDB | API.MongoClusters; + + isServerless?: boolean; } diff --git a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts index ae7270bcd..3e6d9df38 100644 --- a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts +++ b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts @@ -95,6 +95,7 @@ export class MongoClustersBranchDataProvider // 1. extract the basic info from the element (subscription, resource group, etc., provided by Azure Resources) let clusterInfo: MongoClusterModel = element as MongoClusterModel; + clusterInfo.dbExperience = API.MongoClusters; // 2. lookup the details in the cache, on subsequent refreshes, the details will be available in the cache if (this.detailsCache.has(clusterInfo.id)) { @@ -141,6 +142,7 @@ export class MongoClustersBranchDataProvider accounts.map((MongoClustersAccount) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.detailsCache.set(nonNullProp(MongoClustersAccount, 'id'), { + dbExperience: API.MongoClusters, id: MongoClustersAccount.id as string, name: MongoClustersAccount.name as string, resourceGroup: getResourceGroupFromId(MongoClustersAccount.id as string), diff --git a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts index e29c37760..3f6876761 100644 --- a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts @@ -5,6 +5,7 @@ import { createGenericElement, type TreeElementBase } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; +import { API } from '../../../AzureDBExperiences'; import { WorkspaceResourceType } from '../../../tree/workspace/sharedWorkspaceResourceProvider'; import { SharedWorkspaceStorage } from '../../../tree/workspace/sharedWorkspaceStorage'; import { type MongoClusterModel } from '../MongoClusterModel'; @@ -25,6 +26,7 @@ export class MongoDBAccountsWorkspaceItem implements TreeElementBase { const model: MongoClusterModel = { id: item.id, name: item.name, + dbExperience: API.MongoClusters, connectionString: item?.secrets?.[0] ?? undefined, }; return new MongoClusterWorkspaceItem(model); diff --git a/src/tree/mongo/DatabaseItem.ts b/src/tree/mongo/DatabaseItem.ts deleted file mode 100644 index e4c575b14..000000000 --- a/src/tree/mongo/DatabaseItem.ts +++ /dev/null @@ -1,62 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createGenericElement, type TreeElementBase } from '@microsoft/vscode-azext-utils'; -import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type IDatabaseInfo } from './IDatabaseInfo'; -import { type MongoAccountModel } from './MongoAccountModel'; - -export class DatabaseItem { - id: string; - - constructor( - readonly account: MongoAccountModel, - readonly databaseInfo: IDatabaseInfo, - ) { - this.id = `${account.id}/${databaseInfo.name}`; - } - - async getChildren(): Promise { - return [ - createGenericElement({ - contextValue: 'mongoClusters.item.no-collection', - id: `${this.id}/no-databases`, - label: 'Create collection...', - commandId: 'command.mongoClusters.createCollection', - commandArgs: [this], - }), - ]; - } - // const client: MongoClustersClient = await MongoClustersClient.getClient(this.mongoCluster.id); - // const collections = await client.listCollections(this.databaseInfo.name); - - // if (collections.length === 0) { - // // no databases in there: - // return [ - // createGenericElement({ - // contextValue: 'mongoClusters.item.no-collection', - // id: `${this.id}/no-databases`, - // label: 'Create collection...', - // iconPath: new vscode.ThemeIcon('plus'), - // commandId: 'command.mongoClusters.createCollection', - // commandArgs: [this], - // }), - // ]; - // } - - // return collections.map((collection) => { - // return new CollectionItem(this.mongoCluster, this.databaseInfo, collection); - // }); - - getTreeItem(): TreeItem { - return { - id: this.id, - contextValue: 'mongoClusters.item.database', - label: this.databaseInfo.name, - iconPath: new ThemeIcon('database'), - collapsibleState: TreeItemCollapsibleState.Collapsed, - }; - } -} diff --git a/src/tree/mongo/IDatabaseInfo.ts b/src/tree/mongo/IDatabaseInfo.ts index d52a50255..810b4b60a 100644 --- a/src/tree/mongo/IDatabaseInfo.ts +++ b/src/tree/mongo/IDatabaseInfo.ts @@ -4,6 +4,6 @@ *--------------------------------------------------------------------------------------------*/ export interface IDatabaseInfo { - name?: string; + name: string; empty?: boolean; } diff --git a/src/tree/mongo/MongoAccountModel.ts b/src/tree/mongo/MongoAccountModel.ts index 048d0c342..3530e3f89 100644 --- a/src/tree/mongo/MongoAccountModel.ts +++ b/src/tree/mongo/MongoAccountModel.ts @@ -3,10 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { type MongoClusterModel } from '../../mongoClusters/tree/MongoClusterModel'; import { type CosmosAccountModel } from '../CosmosAccountModel'; -export interface MongoAccountModel extends CosmosAccountModel { - // whaterver needed to be added - connectionString?: string; - isServerless?: boolean; -} +export type MongoAccountModel = CosmosAccountModel & MongoClusterModel; diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index d9ffe7d17..8d4fd7b33 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -5,31 +5,33 @@ import { type DatabaseAccountGetResults } from '@azure/arm-cosmosdb'; import { - appendExtensionUserAgent, callWithTelemetryAndErrorHandling, nonNullProp, parseError, type IActionContext, - type TreeElementBase, + type TreeElementBase } from '@microsoft/vscode-azext-utils'; import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; import { type MongoClient } from 'mongodb'; -import { Links, testDb } from '../../constants'; +import ConnectionString from 'mongodb-connection-string-url'; +import { Links } from '../../constants'; import { ext } from '../../extensionVariables'; -import { connectToMongoClient } from '../../mongo/connectToMongoClient'; import { getDatabaseNameFromConnectionString } from '../../mongo/mongoConnectionStrings'; +import { CredentialCache } from '../../mongoClusters/CredentialCache'; +import { MongoClustersClient, type DatabaseItemModel } from '../../mongoClusters/MongoClustersClient'; +import { DatabaseItem } from '../../mongoClusters/tree/DatabaseItem'; +import { type MongoClusterModel } from '../../mongoClusters/tree/MongoClusterModel'; import { createCosmosDBManagementClient } from '../../utils/azureClients'; import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; -import { DatabaseItem } from './DatabaseItem'; import { type IDatabaseInfo } from './IDatabaseInfo'; import { type MongoAccountModel } from './MongoAccountModel'; export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { constructor( protected account: MongoAccountModel, - protected subscription?: AzureSubscription, // optional for the case of a workspace connection - readonly databaseAccount?: DatabaseAccountGetResults, - readonly isEmulator?: boolean, + protected subscription?: AzureSubscription, // available when the account is a azure-resource one + readonly databaseAccount?: DatabaseAccountGetResults, // TODO: exploring during v1->v2 migration + readonly isEmulator?: boolean, // TODO: exploring during v1->v2 migration ) { super(account); } @@ -76,11 +78,14 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { let mongoClient: MongoClient | undefined; try { - let databases: IDatabaseInfo[]; + let databases: DatabaseItemModel[]; if (!this.account.connectionString) { if (this.subscription) { const cString = await this.discoverConnectionString(); + if (!cString) { + throw new Error('Failed to discover the connection string.'); + } this.account.connectionString = cString; } if (!this.account.connectionString) { @@ -88,11 +93,42 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { } } - // Azure MongoDB accounts need to have the name passed in for private endpoints - mongoClient = await connectToMongoClient( - this.account.connectionString, - this.databaseAccount ? nonNullProp(this.databaseAccount, 'name') : appendExtensionUserAgent(), - ); + let mongoClient: MongoClustersClient | null; + + // Check if credentials are cached, and return the cached client if available + if (CredentialCache.hasCredentials(this.id)) { + ext.outputChannel.appendLine(`MongoDB (RU): Reusing active connection for "${this.account.name}".`); + mongoClient = await MongoClustersClient.getClient(this.id); + } else { + // Call to the abstract method to authenticate and connect to the cluster + const cString = new ConnectionString(this.account.connectionString); + const username: string | undefined = cString.username; + const password: string | undefined = cString.password; + CredentialCache.setCredentials(this.id, cString.toString(), username, password); + + try { + mongoClient = await MongoClustersClient.getClient(this.id).catch((error: Error) => { + ext.outputChannel.appendLine('failed.'); + ext.outputChannel.appendLine(`Error: ${error.message}`); + + throw error; + }); + } catch (error) { + console.error(error); + // If connection fails, remove cached credentials + await MongoClustersClient.deleteClient(this.id); + CredentialCache.deleteCredentials(this.id); + + // Return null to indicate failure + return []; + } + } + + // // Azure MongoDB accounts need to have the name passed in for private endpoints + // mongoClient = await connectToMongoClient( + // this.account.connectionString, + // this.databaseAccount ? nonNullProp(this.databaseAccount, 'name') : appendExtensionUserAgent(), + // ); const databaseInConnectionString = getDatabaseNameFromConnectionString(this.account.connectionString); if (databaseInConnectionString && !this.isEmulator) { @@ -107,15 +143,23 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { } else { // https://mongodb.github.io/node-mongodb-native/3.1/api/index.html // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const result: { databases: IDatabaseInfo[] } = await mongoClient.db(testDb).admin().listDatabases(); - databases = result.databases; + databases = await mongoClient.listDatabases(); } return databases .filter( - (database: IDatabaseInfo) => - !(database.name && database.name.toLowerCase() === 'admin' && database.empty), + (databaseInfo: IDatabaseInfo) => + !(databaseInfo.name && databaseInfo.name.toLowerCase() === 'admin' && databaseInfo.empty), ) // Filter out the 'admin' database if it's empty - .map((database) => new DatabaseItem(this.account, database)); + .map((database) => { + const clusterInfo = this.account as MongoClusterModel; + // eslint-disable-next-line no-unused-vars + const databaseInfo: DatabaseItemModel = { + name: database.name, + empty: database.empty, + }; + + return new DatabaseItem(clusterInfo, databaseInfo); + }); } catch (error) { const message = parseError(error).message; if (this.isEmulator && message.includes('ECONNREFUSED')) { From 7f55aaab96aa1b96d6db9ec932de4a82ef4ffef7 Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Mon, 16 Dec 2024 13:01:20 +0100 Subject: [PATCH 05/42] feat: Migrating TreeView to V2 --- package-lock.json | 45 ++--- package.json | 1 + src/extension.ts | 4 +- src/tree/CosmosAccountModel.ts | 24 ++- src/tree/CosmosAccountResourceItemBase.ts | 20 +- src/tree/CosmosBranchDataProvider.ts | 147 --------------- src/tree/CosmosDBBranchDataProvider.ts | 210 +++++++++++++++++++++ src/tree/CosmosDbTreeElement.ts | 14 ++ src/tree/graph/GraphAccountModel.ts | 8 + src/tree/graph/GraphAccountResourceItem.ts | 21 +++ src/tree/nosql/NoSqlAccountModel.ts | 6 +- src/tree/nosql/NoSqlAccountResourceItem.ts | 9 +- src/tree/table/TableAccountModel.ts | 8 + src/tree/table/TableAccountResourceItem.ts | 20 ++ 14 files changed, 339 insertions(+), 198 deletions(-) delete mode 100644 src/tree/CosmosBranchDataProvider.ts create mode 100644 src/tree/CosmosDBBranchDataProvider.ts create mode 100644 src/tree/CosmosDbTreeElement.ts create mode 100644 src/tree/graph/GraphAccountModel.ts create mode 100644 src/tree/graph/GraphAccountResourceItem.ts create mode 100644 src/tree/table/TableAccountModel.ts create mode 100644 src/tree/table/TableAccountResourceItem.ts diff --git a/package-lock.json b/package-lock.json index 351a661b1..0fc3f9e73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@azure/arm-cosmosdb": "16.0.0-beta.7", "@azure/arm-postgresql": "^6.1.0", "@azure/arm-postgresql-flexible": "^7.1.0", + "@azure/arm-resources": "^5.2.0", "@azure/cosmos": "^4.1.1", "@fluentui/react-components": "^9.56.2", "@fluentui/react-icons": "^2.0.265", @@ -269,6 +270,23 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" }, + "node_modules/@azure/arm-resources": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@azure/arm-resources/-/arm-resources-5.2.0.tgz", + "integrity": "sha512-wQyuhL8WQsLkW/KMdik8bLJIJCz3Z6mg/+AKm0KedgK73SKhicSqYP+ed3t+43tLlRFltcrmGKMcHLQ+Jhv/6A==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.7.0", + "@azure/core-lro": "^2.5.0", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@azure/arm-resources-profile-2020-09-01-hybrid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@azure/arm-resources-profile-2020-09-01-hybrid/-/arm-resources-profile-2020-09-01-hybrid-2.0.0.tgz", @@ -311,6 +329,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" }, + "node_modules/@azure/arm-resources/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/@azure/arm-storage": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/@azure/arm-storage/-/arm-storage-18.2.0.tgz", @@ -4286,28 +4309,6 @@ "@azure/ms-rest-azure-env": "^2.0.0" } }, - "node_modules/@microsoft/vscode-azext-azureutils/node_modules/@azure/arm-resources": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@azure/arm-resources/-/arm-resources-5.2.0.tgz", - "integrity": "sha512-wQyuhL8WQsLkW/KMdik8bLJIJCz3Z6mg/+AKm0KedgK73SKhicSqYP+ed3t+43tLlRFltcrmGKMcHLQ+Jhv/6A==", - "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.7.0", - "@azure/core-lro": "^2.5.0", - "@azure/core-paging": "^1.2.0", - "@azure/core-rest-pipeline": "^1.8.0", - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@microsoft/vscode-azext-azureutils/node_modules/tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" - }, "node_modules/@microsoft/vscode-azext-azureutils/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", diff --git a/package.json b/package.json index 0ec47ebbd..a2ebeac89 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ }, "dependencies": { "@azure/arm-cosmosdb": "16.0.0-beta.7", + "@azure/arm-resources": "^5.2.0", "@azure/arm-postgresql": "^6.1.0", "@azure/arm-postgresql-flexible": "^7.1.0", "@azure/cosmos": "^4.1.1", diff --git a/src/extension.ts b/src/extension.ts index 6236c9e62..5e664d6b7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -59,7 +59,7 @@ import { DatabaseResolver } from './resolver/AppResolver'; import { DatabaseWorkspaceProvider } from './resolver/DatabaseWorkspaceProvider'; import { TableAccountTreeItem } from './table/tree/TableAccountTreeItem'; import { AttachedAccountSuffix } from './tree/AttachedAccountsTreeItem'; -import { CosmosBranchDataProvider } from './tree/CosmosBranchDataProvider'; +import { CosmosDBBranchDataProvider } from './tree/CosmosDBBranchDataProvider'; import { SubscriptionTreeItem } from './tree/SubscriptionTreeItem'; import { localize } from './utils/localize'; @@ -100,7 +100,7 @@ export async function activateInternal( // ext.rgApi.registerApplicationResourceResolver(AzExtResourceType.AzureCosmosDb, new DatabaseResolver()); ext.rgApiV2.resources.registerAzureResourceBranchDataProvider( AzExtResourceType.AzureCosmosDb, - new CosmosBranchDataProvider(), + new CosmosDBBranchDataProvider(), ); ext.rgApi.registerApplicationResourceResolver( diff --git a/src/tree/CosmosAccountModel.ts b/src/tree/CosmosAccountModel.ts index fa45ef804..3c6795f2b 100644 --- a/src/tree/CosmosAccountModel.ts +++ b/src/tree/CosmosAccountModel.ts @@ -3,15 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type Resource } from '@azure/arm-cosmosdb'; -import { type API } from '../AzureDBExperiences'; +import { type GenericResource } from '@azure/arm-resources'; +import { type AzureResource } from '@microsoft/vscode-azureresources-api'; +import { type Experience } from '../AzureDBExperiences'; -export interface CosmosAccountModel extends Resource { - id: string; - name: string; +/** + * Cosmos DB resource + * Azure Resource group library mixes the raw generic resource into AzureResource + * Therefore, we can access the raw generic resource from the CosmosDBResource + * However, ideally we have to use raw property to access to the Cosmos DB resource + */ +export type CosmosDBResource = AzureResource & + GenericResource & { + readonly raw: GenericResource; // Resource object from Azure SDK + }; - dbExperience: API; - - // introduced new properties - resourceGroup?: string; +export interface CosmosAccountModel extends CosmosDBResource { + dbExperience: Experience; // Cosmos DB Experience } diff --git a/src/tree/CosmosAccountResourceItemBase.ts b/src/tree/CosmosAccountResourceItemBase.ts index e882619c2..d66874146 100644 --- a/src/tree/CosmosAccountResourceItemBase.ts +++ b/src/tree/CosmosAccountResourceItemBase.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type TreeElementBase } from '@microsoft/vscode-azext-utils'; -import { type TreeItem } from 'vscode'; - import * as vscode from 'vscode'; -import { getExperienceFromApi } from '../AzureDBExperiences'; +import { type TreeItem } from 'vscode'; import { type CosmosAccountModel } from './CosmosAccountModel'; +import { type CosmosDbTreeElement } from './CosmosDbTreeElement'; -export abstract class CosmosAccountResourceItemBase implements TreeElementBase { - id: string; +export abstract class CosmosAccountResourceItemBase implements CosmosDbTreeElement { + public id: string; + public readonly account: CosmosAccountModel; - constructor(public cosmosAccount: CosmosAccountModel) { + protected constructor(cosmosAccount: CosmosAccountModel) { this.id = cosmosAccount.id ?? ''; + this.account = cosmosAccount; } /** @@ -24,9 +24,9 @@ export abstract class CosmosAccountResourceItemBase implements TreeElementBase { getTreeItem(): TreeItem { return { id: this.id, - contextValue: `${this.cosmosAccount.dbExperience}.item.account`, - label: this.cosmosAccount.name, - description: `(${getExperienceFromApi(this.cosmosAccount.dbExperience).shortName})`, + contextValue: `${this.account.dbExperience.api}.item.account`, + label: this.account.name, + description: `(${this.account.dbExperience.shortName})`, //iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg'), // Uncomment if icon is available collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }; diff --git a/src/tree/CosmosBranchDataProvider.ts b/src/tree/CosmosBranchDataProvider.ts deleted file mode 100644 index f74b4584d..000000000 --- a/src/tree/CosmosBranchDataProvider.ts +++ /dev/null @@ -1,147 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { - type AzureResource, - type AzureResourceBranchDataProvider, - type ResourceModelBase, -} from '@microsoft/vscode-azureresources-api'; -import * as vscode from 'vscode'; -import { API } from '../AzureDBExperiences'; -import { ext } from '../extensionVariables'; -import { type MongoAccountModel } from './mongo/MongoAccountModel'; -import { MongoAccountResourceItem } from './mongo/MongoAccountResourceItem'; -import { type NoSqlAccountModel } from './nosql/NoSqlAccountModel'; -import { NoSqlAccountResourceItem } from './nosql/NoSqlAccountResourceItem'; - -const resourceTypes = [ - 'microsoft.documentdb/databaseaccounts', // then, investigate .kind for "MongoDB" - 'microsoft.dbforpostgresql/servers', - 'microsoft.dbforpostgresql/flexibleservers', -]; - -export interface TreeElementBase extends ResourceModelBase { - getChildren?(): vscode.ProviderResult; - getTreeItem(): vscode.TreeItem | Thenable; -} - -export class CosmosBranchDataProvider - extends vscode.Disposable - implements AzureResourceBranchDataProvider -{ - private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter(); - - constructor() { - super(() => { - this.onDidChangeTreeDataEmitter.dispose(); - }); - } - - get onDidChangeTreeData(): vscode.Event { - return this.onDidChangeTreeDataEmitter.event; - } - - async getChildren(element: TreeElementBase): Promise { - /** - * getChildren is called for every element in the tree when expanding, the element being expanded is being passed as an argument - */ - return await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - //context.telemetry.properties.experience = API.MongoClusters; - context.telemetry.properties.parentContext = (await element.getTreeItem()).contextValue ?? 'unknown'; - - return (await element.getChildren?.())?.map((child) => { - if (child.id) { - return ext.state.wrapItemInStateHandling(child as TreeElementBase & { id: string }, () => - this.refresh(child), - ); - } - return child; - }); - }); - } - - async getResourceItem(element: AzureResource): Promise { - /** - * This function is being called when the resource tree is being built, it is called for every resource element in the tree. - */ - - const resourceItem = await callWithTelemetryAndErrorHandling( - 'resolveResource', - // disabling require-await, the async aspect is in there, but uses the .then pattern - // eslint-disable-next-line @typescript-eslint/require-await - async (context: IActionContext) => { - switch (element.azureResourceType.type.toLowerCase()) { - case resourceTypes[0]: { - if (element.azureResourceType.kinds?.includes('mongodb')) { - context.telemetry.properties.experience = API.MongoDB; - - // 1. extract the basic info from the element (subscription, resource group, etc., provided by Azure Resources) - const accountInfo: MongoAccountModel = element as unknown as MongoAccountModel; - accountInfo.dbExperience = API.MongoDB; - - const item = new MongoAccountResourceItem(accountInfo, element.subscription); - - return item; - } else { - // TODO: just "else"? really? explore the other options for 'kind', don't we have table, graphapi etc. in there?? - context.telemetry.properties.experience = API.Core; // TODO: verify whether 'else' is still a good choice here - - // 1. extract the basic info from the element (subscription, resource group, etc., provided by Azure Resources) - const accountInfo: NoSqlAccountModel = element as unknown as NoSqlAccountModel; - accountInfo.dbExperience = API.Core; - - const item = new NoSqlAccountResourceItem(accountInfo, element.subscription); - - return item; - } - - // const client = await createCosmosDBClient({ ...context, ...subContext }); - // const databaseAccount = await client.databaseAccounts.get(resourceGroupName, name); - // dbChild = await SubscriptionTreeItem.initCosmosDBChild( - // client, - // databaseAccount, - // nonNullValue(subNode), - // ); - // const experience = tryGetExperience(databaseAccount); - - // return experience?.api === API.MongoDB - // ? new ResolvedMongoAccountResource(dbChild as MongoAccountTreeItem, resource) - // : new ResolvedDocDBAccountResource(dbChild as DocDBAccountTreeItem, resource); - return null; - break; - } - case resourceTypes[1]: - case resourceTypes[2]: { - // const postgresClient = - // resource.type.toLowerCase() === resourceTypes[1] - // ? await createPostgreSQLClient({ ...context, ...subContext }) - // : await createPostgreSQLFlexibleClient({ ...context, ...subContext }); - - // postgresServer = await postgresClient.servers.get(resourceGroupName, name); - // dbChild = await SubscriptionTreeItem.initPostgresChild(postgresServer, nonNullValue(subNode)); - - // return new ResolvedPostgresServerResource(dbChild as PostgresServerTreeItem, resource); - return null; - } - default: - return null; - } - }, - ); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return ext.state.wrapItemInStateHandling(resourceItem!, () => this.refresh(resourceItem as TreeElementBase)); - } - - async getTreeItem(element: TreeElementBase): Promise { - const ti = await element.getTreeItem(); - return ti; - } - - refresh(element?: TreeElementBase): void { - this.onDidChangeTreeDataEmitter.fire(element); - } -} diff --git a/src/tree/CosmosDBBranchDataProvider.ts b/src/tree/CosmosDBBranchDataProvider.ts new file mode 100644 index 000000000..9038b22d9 --- /dev/null +++ b/src/tree/CosmosDBBranchDataProvider.ts @@ -0,0 +1,210 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosDBManagementClient, type DatabaseAccountGetResults } from '@azure/arm-cosmosdb'; +import { type DatabaseAccountListKeysResult } from '@azure/arm-cosmosdb/src/models'; +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; +import * as vscode from 'vscode'; +import { API, tryGetExperience } from '../AzureDBExperiences'; +import { databaseAccountType } from '../constants'; +import { type CosmosDBCredential, type CosmosDBKeyCredential } from '../docdb/getCosmosClient'; +import { ext } from '../extensionVariables'; +import { tryGetGremlinEndpointFromAzure } from '../graph/gremlinEndpoints'; +import { createCosmosDBManagementClient } from '../utils/azureClients'; +import { localize } from '../utils/localize'; +import { nonNullProp } from '../utils/nonNull'; +import { type CosmosAccountModel, type CosmosDBResource } from './CosmosAccountModel'; +import { type CosmosDbTreeElement } from './CosmosDbTreeElement'; +import { GraphAccountResourceItem } from './graph/GraphAccountResourceItem'; +import { type MongoAccountModel } from './mongo/MongoAccountModel'; +import { MongoAccountResourceItem } from './mongo/MongoAccountResourceItem'; +import { NoSqlAccountResourceItem } from './nosql/NoSqlAccountResourceItem'; +import { TableAccountResourceItem } from './table/TableAccountResourceItem'; + +export class CosmosDBBranchDataProvider + extends vscode.Disposable + implements BranchDataProvider +{ + private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter(); + + constructor() { + super(() => this.onDidChangeTreeDataEmitter.dispose()); + } + + get onDidChangeTreeData(): vscode.Event { + return this.onDidChangeTreeDataEmitter.event; + } + + /** + * This function is called for every element in the tree when expanding, the element being expanded is being passed as an argument + */ + async getChildren(element: CosmosDbTreeElement): Promise { + const result = await callWithTelemetryAndErrorHandling( + 'CosmosDBBranchDataProvider.getChildren', + async (context: IActionContext) => { + const elementTreeItem = await element.getTreeItem(); + + context.telemetry.properties.parentContext = elementTreeItem.contextValue ?? 'unknown'; + + return (await element.getChildren?.())?.map((child) => { + return ext.state.wrapItemInStateHandling(child, (child: CosmosDbTreeElement) => + this.refresh(child), + ) as CosmosDbTreeElement; + }); + }, + ); + + return result ?? []; + } + + /** + * This function is being called when the resource tree is being built, it is called for every top level of resources. + * @param resource + */ + async getResourceItem(resource: CosmosDBResource): Promise { + const resourceItem = await callWithTelemetryAndErrorHandling( + 'CosmosDBBranchDataProvider.getResourceItem', + async (context: IActionContext) => { + const id = nonNullProp(resource, 'id'); + const name = nonNullProp(resource, 'name'); + const type = nonNullProp(resource, 'type'); + const resourceGroup = nonNullProp(resource, 'resourceGroup'); + + context.valuesToMask.push(id); + context.valuesToMask.push(name); + + if (type.toLocaleLowerCase() === databaseAccountType.toLocaleLowerCase()) { + if (resource.subscription) { + // Tree view has subscription + const accountModel = resource as CosmosAccountModel; + const client = await createCosmosDBManagementClient(context, resource.subscription); + const databaseAccount = await client.databaseAccounts.get(resourceGroup, name); + const experience = tryGetExperience(databaseAccount); + const credentials = await this.getCredentials(name, resourceGroup, client, databaseAccount); + const documentEndpoint: string = nonNullProp( + databaseAccount, + 'documentEndpoint', + `of the database account ${id}`, + ); + + if (experience) { + // TODO: Should we change the input element? Probably will be better to create a new one + accountModel.dbExperience = experience; + } + + if (experience?.api === API.MongoDB) { + return new MongoAccountResourceItem( + accountModel as MongoAccountModel, + resource.subscription, + ); + } + + if (experience?.api === API.Core) { + return new NoSqlAccountResourceItem(accountModel, credentials, documentEndpoint); + } + + if (experience?.api === API.Graph) { + const gremlinEndpoint = await tryGetGremlinEndpointFromAzure(client, resourceGroup, name); + return new GraphAccountResourceItem( + accountModel, + credentials, + documentEndpoint, + gremlinEndpoint, + ); + } + + if (experience?.api === API.Table) { + return new TableAccountResourceItem(accountModel, credentials, documentEndpoint); + } + } else { + // Workspace view doesn't have subscription. Not supported yet + } + } else { + // Unknown resource type + } + + return null as unknown as CosmosDbTreeElement; + }, + ); + + if (resourceItem) { + return ext.state.wrapItemInStateHandling(resourceItem, (item: CosmosDbTreeElement) => + this.refresh(item), + ) as CosmosDbTreeElement; + } + + return null as unknown as CosmosDbTreeElement; + } + + async getTreeItem(element: CosmosDbTreeElement): Promise { + return element.getTreeItem(); + } + + refresh(element?: CosmosDbTreeElement): void { + this.onDidChangeTreeDataEmitter.fire(element); + } + + private async getCredentials( + name: string, + resourceGroup: string, + client: CosmosDBManagementClient, + databaseAccount: DatabaseAccountGetResults, + ): Promise { + const result = await callWithTelemetryAndErrorHandling( + 'CosmosDBBranchDataProvider.getCredentials', + async (context: IActionContext) => { + const localAuthDisabled = databaseAccount.disableLocalAuth === true; + const forceOAuth = vscode.workspace.getConfiguration().get('azureDatabases.useCosmosOAuth'); + context.telemetry.properties.useCosmosOAuth = (forceOAuth ?? false).toString(); + + let keyCred: CosmosDBKeyCredential | undefined = undefined; + // disable key auth if the user has opted in to OAuth (AAD/Entra ID) + if (!forceOAuth) { + try { + context.telemetry.properties.localAuthDisabled = localAuthDisabled.toString(); + + let keyResult: DatabaseAccountListKeysResult | undefined; + // If the account has local auth disabled, don't even try to use key auth + if (!localAuthDisabled) { + keyResult = await client.databaseAccounts.listKeys(resourceGroup, name); + keyCred = keyResult?.primaryMasterKey + ? { + type: 'key', + key: keyResult.primaryMasterKey, + } + : undefined; + context.telemetry.properties.receivedKeyCreds = 'true'; + } else { + throw new Error('Local auth is disabled'); + } + } catch { + context.telemetry.properties.receivedKeyCreds = 'false'; + const message = localize( + 'keyPermissionErrorMsg', + 'You do not have the required permissions to list auth keys for [{0}].\nFalling back to using Entra ID.\nYou can change the default authentication in the settings.', + name, + ); + const openSettingsItem = localize('openSettings', 'Open Settings'); + void vscode.window.showWarningMessage(message, ...[openSettingsItem]).then((item) => { + if (item === openSettingsItem) { + void vscode.commands.executeCommand( + 'workbench.action.openSettings', + 'azureDatabases.useCosmosOAuth', + ); + } + }); + } + } + + // OAuth is always enabled for Cosmos DB and will be used as a fallback if key auth is unavailable + const authCred = { type: 'auth' }; + return [keyCred, authCred].filter((cred): cred is CosmosDBCredential => cred !== undefined); + }, + ); + + return result ?? []; + } +} diff --git a/src/tree/CosmosDbTreeElement.ts b/src/tree/CosmosDbTreeElement.ts new file mode 100644 index 000000000..aa1f84516 --- /dev/null +++ b/src/tree/CosmosDbTreeElement.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type TreeElementWithId } from '@microsoft/vscode-azext-utils'; +import type * as vscode from 'vscode'; + +export interface ExtTreeElementBase extends TreeElementWithId { + getChildren?(): vscode.ProviderResult; + getTreeItem(): vscode.TreeItem | Thenable; +} + +export type CosmosDbTreeElement = ExtTreeElementBase; diff --git a/src/tree/graph/GraphAccountModel.ts b/src/tree/graph/GraphAccountModel.ts new file mode 100644 index 000000000..050e7f504 --- /dev/null +++ b/src/tree/graph/GraphAccountModel.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosAccountModel } from '../CosmosAccountModel'; + +export type GraphAccountModel = CosmosAccountModel; diff --git a/src/tree/graph/GraphAccountResourceItem.ts b/src/tree/graph/GraphAccountResourceItem.ts new file mode 100644 index 000000000..00f153118 --- /dev/null +++ b/src/tree/graph/GraphAccountResourceItem.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosDBCredential } from '../../docdb/getCosmosClient'; +import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; +import { type GraphAccountModel } from './GraphAccountModel'; + +export class GraphAccountResourceItem extends CosmosAccountResourceItemBase { + constructor( + account: GraphAccountModel, + private readonly credentials: CosmosDBCredential[], + private readonly documentEndpoint: string, + private readonly gremlinEndpoint: string, + ) { + super(account); + } + + // here, we can add more methods or properties specific to MongoDB +} diff --git a/src/tree/nosql/NoSqlAccountModel.ts b/src/tree/nosql/NoSqlAccountModel.ts index 4669c3136..cca4a39a9 100644 --- a/src/tree/nosql/NoSqlAccountModel.ts +++ b/src/tree/nosql/NoSqlAccountModel.ts @@ -5,8 +5,4 @@ import { type CosmosAccountModel } from '../CosmosAccountModel'; -export interface NoSqlAccountModel extends CosmosAccountModel { - // whaterver needed to be added - connectionString?: string; - isServerless?: boolean; -} +export type NoSqlAccountModel = CosmosAccountModel; diff --git a/src/tree/nosql/NoSqlAccountResourceItem.ts b/src/tree/nosql/NoSqlAccountResourceItem.ts index 055ba00a7..8a6bfc4a1 100644 --- a/src/tree/nosql/NoSqlAccountResourceItem.ts +++ b/src/tree/nosql/NoSqlAccountResourceItem.ts @@ -3,17 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; +import { type CosmosDBCredential } from '../../docdb/getCosmosClient'; import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; import { type NoSqlAccountModel } from './NoSqlAccountModel'; export class NoSqlAccountResourceItem extends CosmosAccountResourceItemBase { constructor( account: NoSqlAccountModel, - /**private*/ readonly subscription?: AzureSubscription, // optional for the case of a workspace connection | private commented out to keep the compiler happy for now + private readonly credentials: CosmosDBCredential[], + private readonly documentEndpoint: string, ) { - console.log(subscription ? subscription.subscriptionId : 'No subscription'); super(account); + // + // // Default to DocumentDB, the base type for all Cosmos DB Accounts + // return new DocDBAccountTreeItem(parent, id, label, documentEndpoint, credentials, isEmulator, databaseAccount); } // here, we can add more methods or properties specific to MongoDB diff --git a/src/tree/table/TableAccountModel.ts b/src/tree/table/TableAccountModel.ts new file mode 100644 index 000000000..b4d8bd397 --- /dev/null +++ b/src/tree/table/TableAccountModel.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosAccountModel } from '../CosmosAccountModel'; + +export type TableAccountModel = CosmosAccountModel; diff --git a/src/tree/table/TableAccountResourceItem.ts b/src/tree/table/TableAccountResourceItem.ts new file mode 100644 index 000000000..e72804e97 --- /dev/null +++ b/src/tree/table/TableAccountResourceItem.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosDBCredential } from '../../docdb/getCosmosClient'; +import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; +import { type TableAccountModel } from './TableAccountModel'; + +export class TableAccountResourceItem extends CosmosAccountResourceItemBase { + constructor( + account: TableAccountModel, + private readonly credentials: CosmosDBCredential[], + private readonly documentEndpoint: string, + ) { + super(account); + } + + // here, we can add more methods or properties specific to MongoDB +} From bc53d00b69572b4c53b623d91f0c98778486ddfd Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Tue, 17 Dec 2024 12:10:47 +0100 Subject: [PATCH 06/42] feat: Migrating TreeView to V2 --- src/AzureDBExperiences.ts | 81 +++++++++--- src/tree/CosmosAccountModel.ts | 13 +- src/tree/CosmosAccountResourceItemBase.ts | 31 +++-- src/tree/CosmosDBBranchDataProvider.ts | 136 ++++----------------- src/tree/DocumentDBAccountResourceItem.ts | 114 +++++++++++++++++ src/tree/graph/GraphAccountResourceItem.ts | 30 +++-- src/tree/mongo/DatabaseItem.ts | 9 +- src/tree/mongo/MongoAccountResourceItem.ts | 10 +- src/tree/nosql/NoSqlAccountResourceItem.ts | 19 +-- src/tree/table/TableAccountResourceItem.ts | 24 ++-- 10 files changed, 276 insertions(+), 191 deletions(-) create mode 100644 src/tree/DocumentDBAccountResourceItem.ts diff --git a/src/AzureDBExperiences.ts b/src/AzureDBExperiences.ts index 454e2bdf2..5ee77c773 100644 --- a/src/AzureDBExperiences.ts +++ b/src/AzureDBExperiences.ts @@ -5,6 +5,7 @@ import { type DatabaseAccountGetResults } from '@azure/arm-cosmosdb/src/models'; import { type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; +import { type CosmosDBResource } from './tree/CosmosAccountModel'; import { nonNullProp } from './utils/nonNull'; export enum API { @@ -12,6 +13,7 @@ export enum API { MongoClusters = 'MongoClusters', Graph = 'Graph', Table = 'Table', + Cassandra = 'Cassandra', Core = 'Core', // Now called NoSQL PostgresSingle = 'PostgresSingle', PostgresFlexible = 'PostgresFlexible', @@ -22,7 +24,21 @@ export enum DBAccountKind { GlobalDocumentDB = 'GlobalDocumentDB', } -export type CapabilityName = 'EnableGremlin' | 'EnableTable'; +enum Capability { + EnableGremlin = 'EnableGremlin', + EnableTable = 'EnableTable', + EnableCassandra = 'EnableCassandra', +} + +enum Tag { + Core = 'Core (SQL)', + Mongo = 'Azure Cosmos DB for MongoDB API', + Table = 'Azure Table', + Gremlin = 'Gremlin (graph)', + Cassandra = 'Cassandra', +} + +export type CapabilityName = 'EnableGremlin' | 'EnableTable' | 'EnableCassandra'; export function getExperienceFromApi(api: API): Experience { let info = experiencesMap.get(api); @@ -32,30 +48,49 @@ export function getExperienceFromApi(api: API): Experience { return info; } -export function getExperienceLabel(databaseAccount: DatabaseAccountGetResults): string { - const experience: Experience | undefined = tryGetExperience(databaseAccount); +export function getExperienceLabel(resource: CosmosDBResource | DatabaseAccountGetResults): string { + const experience: Experience | undefined = tryGetExperience(resource); if (experience) { return experience.shortName; } // Must be some new kind of resource that we aren't aware of. Try to get a decent label - const defaultExperience: string = ( - (databaseAccount && databaseAccount.tags && databaseAccount.tags.defaultExperience) - ); - const firstCapability = databaseAccount.capabilities && databaseAccount.capabilities[0]; - const firstCapabilityName = firstCapability?.name?.replace(/^Enable/, ''); - return defaultExperience || firstCapabilityName || nonNullProp(databaseAccount, 'kind'); + const defaultExperience: string = resource?.tags?.defaultExperience ?? ''; + + if ('capabilities' in resource) { + const firstCapability = resource?.capabilities?.[0] ?? {}; + const firstCapabilityName = firstCapability?.name?.replace(/^Enable/, ''); + return firstCapabilityName || nonNullProp(resource, 'kind'); + } + + return defaultExperience || nonNullProp(resource, 'kind'); } -export function tryGetExperience(resource: DatabaseAccountGetResults): Experience | undefined { - // defaultExperience in the resource doesn't really mean anything, we can't depend on its value for determining resource type +export function tryGetExperience(resource: CosmosDBResource | DatabaseAccountGetResults): Experience | undefined { if (resource.kind === DBAccountKind.MongoDB) { return MongoExperience; - } else if (resource.capabilities?.find((cap) => cap.name === 'EnableGremlin')) { - return GremlinExperience; - } else if (resource.capabilities?.find((cap) => cap.name === 'EnableTable')) { - return TableExperience; - } else if (resource.capabilities?.length === 0) { - return CoreExperience; + } + + if ('capabilities' in resource) { + // defaultExperience in the resource doesn't really mean anything, we can't depend on its value for determining resource type + if (resource.capabilities?.find((cap) => cap.name === Capability.EnableGremlin)) { + return GremlinExperience; + } else if (resource.capabilities?.find((cap) => cap.name === Capability.EnableTable)) { + return TableExperience; + } else if (resource.capabilities?.find((cap) => cap.name === Capability.EnableCassandra)) { + return CassandraExperience; + } else if (resource.capabilities?.length === 0) { + return CoreExperience; + } + } else if ('tags' in resource) { + if (resource.tags?.defaultExperience === Tag.Gremlin) { + return GremlinExperience; + } else if (resource.tags?.defaultExperience === Tag.Table) { + return TableExperience; + } else if (resource.tags?.defaultExperience === Tag.Cassandra) { + return CassandraExperience; + } else if (resource.tags?.defaultExperience === Tag.Core) { + return CoreExperience; + } } return undefined; @@ -139,12 +174,20 @@ export const GremlinExperience: Experience = { capability: 'EnableGremlin', tag: 'Gremlin (graph)', } as const; -const PostgresSingleExperience: Experience = { +export const CassandraExperience: Experience = { + api: API.Cassandra, + longName: 'Cosmos DB for Cassandra', + shortName: 'Cassandra', + kind: DBAccountKind.GlobalDocumentDB, + capability: 'EnableCassandra', + tag: 'Cassandra', +}; +export const PostgresSingleExperience: Experience = { api: API.PostgresSingle, longName: 'PostgreSQL Single Server', shortName: 'PostgreSQLSingle', }; -const PostgresFlexibleExperience: Experience = { +export const PostgresFlexibleExperience: Experience = { api: API.PostgresFlexible, longName: 'PostgreSQL Flexible Server', shortName: 'PostgreSQLFlexible', diff --git a/src/tree/CosmosAccountModel.ts b/src/tree/CosmosAccountModel.ts index 3c6795f2b..b85ed46fc 100644 --- a/src/tree/CosmosAccountModel.ts +++ b/src/tree/CosmosAccountModel.ts @@ -4,8 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { type GenericResource } from '@azure/arm-resources'; -import { type AzureResource } from '@microsoft/vscode-azureresources-api'; -import { type Experience } from '../AzureDBExperiences'; +import { type AzureResource, type WorkspaceResource } from '@microsoft/vscode-azureresources-api'; + +export type CosmosDBWorkspaceResource = WorkspaceResource; + +export interface CosmosDBWorkspaceModel extends CosmosDBWorkspaceResource { + connectionString?: string; +} /** * Cosmos DB resource @@ -18,6 +23,4 @@ export type CosmosDBResource = AzureResource & readonly raw: GenericResource; // Resource object from Azure SDK }; -export interface CosmosAccountModel extends CosmosDBResource { - dbExperience: Experience; // Cosmos DB Experience -} +export type CosmosAccountModel = CosmosDBResource; diff --git a/src/tree/CosmosAccountResourceItemBase.ts b/src/tree/CosmosAccountResourceItemBase.ts index d66874146..8cadd1ce1 100644 --- a/src/tree/CosmosAccountResourceItemBase.ts +++ b/src/tree/CosmosAccountResourceItemBase.ts @@ -5,16 +5,23 @@ import * as vscode from 'vscode'; import { type TreeItem } from 'vscode'; +import { getExperienceLabel, tryGetExperience } from '../AzureDBExperiences'; import { type CosmosAccountModel } from './CosmosAccountModel'; import { type CosmosDbTreeElement } from './CosmosDbTreeElement'; export abstract class CosmosAccountResourceItemBase implements CosmosDbTreeElement { public id: string; - public readonly account: CosmosAccountModel; - protected constructor(cosmosAccount: CosmosAccountModel) { - this.id = cosmosAccount.id ?? ''; - this.account = cosmosAccount; + protected constructor(protected readonly account: CosmosAccountModel) { + this.id = account.id ?? ''; + } + + /** + * Returns the children of the cluster. + * @returns The children of the cluster. + */ + getChildren(): Promise { + return Promise.resolve([]); } /** @@ -22,12 +29,22 @@ export abstract class CosmosAccountResourceItemBase implements CosmosDbTreeEleme * @returns The TreeItem object. */ getTreeItem(): TreeItem { + const experience = tryGetExperience(this.account); + if (!experience) { + const accountKindLabel = getExperienceLabel(this.account); + const label: string = this.account.name + (accountKindLabel ? ` (${accountKindLabel})` : ``); + return { + id: this.id, + contextValue: 'cosmosDB.item.account', + label: label, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + }; + } return { id: this.id, - contextValue: `${this.account.dbExperience.api}.item.account`, + contextValue: `${experience.api}.item.account`, label: this.account.name, - description: `(${this.account.dbExperience.shortName})`, - //iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg'), // Uncomment if icon is available + description: `(${experience.shortName})`, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }; } diff --git a/src/tree/CosmosDBBranchDataProvider.ts b/src/tree/CosmosDBBranchDataProvider.ts index 9038b22d9..150b3056e 100644 --- a/src/tree/CosmosDBBranchDataProvider.ts +++ b/src/tree/CosmosDBBranchDataProvider.ts @@ -3,21 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type CosmosDBManagementClient, type DatabaseAccountGetResults } from '@azure/arm-cosmosdb'; -import { type DatabaseAccountListKeysResult } from '@azure/arm-cosmosdb/src/models'; import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; import { API, tryGetExperience } from '../AzureDBExperiences'; import { databaseAccountType } from '../constants'; -import { type CosmosDBCredential, type CosmosDBKeyCredential } from '../docdb/getCosmosClient'; import { ext } from '../extensionVariables'; -import { tryGetGremlinEndpointFromAzure } from '../graph/gremlinEndpoints'; -import { createCosmosDBManagementClient } from '../utils/azureClients'; -import { localize } from '../utils/localize'; import { nonNullProp } from '../utils/nonNull'; import { type CosmosAccountModel, type CosmosDBResource } from './CosmosAccountModel'; import { type CosmosDbTreeElement } from './CosmosDbTreeElement'; +import { DocumentDBAccountResourceItem } from './DocumentDBAccountResourceItem'; import { GraphAccountResourceItem } from './graph/GraphAccountResourceItem'; import { type MongoAccountModel } from './mongo/MongoAccountModel'; import { MongoAccountResourceItem } from './mongo/MongoAccountResourceItem'; @@ -71,57 +66,35 @@ export class CosmosDBBranchDataProvider const id = nonNullProp(resource, 'id'); const name = nonNullProp(resource, 'name'); const type = nonNullProp(resource, 'type'); - const resourceGroup = nonNullProp(resource, 'resourceGroup'); context.valuesToMask.push(id); context.valuesToMask.push(name); if (type.toLocaleLowerCase() === databaseAccountType.toLocaleLowerCase()) { - if (resource.subscription) { - // Tree view has subscription - const accountModel = resource as CosmosAccountModel; - const client = await createCosmosDBManagementClient(context, resource.subscription); - const databaseAccount = await client.databaseAccounts.get(resourceGroup, name); - const experience = tryGetExperience(databaseAccount); - const credentials = await this.getCredentials(name, resourceGroup, client, databaseAccount); - const documentEndpoint: string = nonNullProp( - databaseAccount, - 'documentEndpoint', - `of the database account ${id}`, - ); - - if (experience) { - // TODO: Should we change the input element? Probably will be better to create a new one - accountModel.dbExperience = experience; - } - - if (experience?.api === API.MongoDB) { - return new MongoAccountResourceItem( - accountModel as MongoAccountModel, - resource.subscription, - ); - } - - if (experience?.api === API.Core) { - return new NoSqlAccountResourceItem(accountModel, credentials, documentEndpoint); - } - - if (experience?.api === API.Graph) { - const gremlinEndpoint = await tryGetGremlinEndpointFromAzure(client, resourceGroup, name); - return new GraphAccountResourceItem( - accountModel, - credentials, - documentEndpoint, - gremlinEndpoint, - ); - } - - if (experience?.api === API.Table) { - return new TableAccountResourceItem(accountModel, credentials, documentEndpoint); - } - } else { - // Workspace view doesn't have subscription. Not supported yet + const accountModel = resource as CosmosAccountModel; + const experience = tryGetExperience(resource); + + if (experience?.api === API.MongoDB) { + return new MongoAccountResourceItem(accountModel as MongoAccountModel, resource.subscription); + } + + if (experience?.api === API.Cassandra) { + return new DocumentDBAccountResourceItem(accountModel, experience); } + + if (experience?.api === API.Core) { + return new NoSqlAccountResourceItem(accountModel, experience); + } + + if (experience?.api === API.Graph) { + return new GraphAccountResourceItem(accountModel, experience); + } + + if (experience?.api === API.Table) { + return new TableAccountResourceItem(accountModel, experience); + } + + // Unknown experience } else { // Unknown resource type } @@ -146,65 +119,4 @@ export class CosmosDBBranchDataProvider refresh(element?: CosmosDbTreeElement): void { this.onDidChangeTreeDataEmitter.fire(element); } - - private async getCredentials( - name: string, - resourceGroup: string, - client: CosmosDBManagementClient, - databaseAccount: DatabaseAccountGetResults, - ): Promise { - const result = await callWithTelemetryAndErrorHandling( - 'CosmosDBBranchDataProvider.getCredentials', - async (context: IActionContext) => { - const localAuthDisabled = databaseAccount.disableLocalAuth === true; - const forceOAuth = vscode.workspace.getConfiguration().get('azureDatabases.useCosmosOAuth'); - context.telemetry.properties.useCosmosOAuth = (forceOAuth ?? false).toString(); - - let keyCred: CosmosDBKeyCredential | undefined = undefined; - // disable key auth if the user has opted in to OAuth (AAD/Entra ID) - if (!forceOAuth) { - try { - context.telemetry.properties.localAuthDisabled = localAuthDisabled.toString(); - - let keyResult: DatabaseAccountListKeysResult | undefined; - // If the account has local auth disabled, don't even try to use key auth - if (!localAuthDisabled) { - keyResult = await client.databaseAccounts.listKeys(resourceGroup, name); - keyCred = keyResult?.primaryMasterKey - ? { - type: 'key', - key: keyResult.primaryMasterKey, - } - : undefined; - context.telemetry.properties.receivedKeyCreds = 'true'; - } else { - throw new Error('Local auth is disabled'); - } - } catch { - context.telemetry.properties.receivedKeyCreds = 'false'; - const message = localize( - 'keyPermissionErrorMsg', - 'You do not have the required permissions to list auth keys for [{0}].\nFalling back to using Entra ID.\nYou can change the default authentication in the settings.', - name, - ); - const openSettingsItem = localize('openSettings', 'Open Settings'); - void vscode.window.showWarningMessage(message, ...[openSettingsItem]).then((item) => { - if (item === openSettingsItem) { - void vscode.commands.executeCommand( - 'workbench.action.openSettings', - 'azureDatabases.useCosmosOAuth', - ); - } - }); - } - } - - // OAuth is always enabled for Cosmos DB and will be used as a fallback if key auth is unavailable - const authCred = { type: 'auth' }; - return [keyCred, authCred].filter((cred): cred is CosmosDBCredential => cred !== undefined); - }, - ); - - return result ?? []; - } } diff --git a/src/tree/DocumentDBAccountResourceItem.ts b/src/tree/DocumentDBAccountResourceItem.ts new file mode 100644 index 000000000..c073841c8 --- /dev/null +++ b/src/tree/DocumentDBAccountResourceItem.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosDBManagementClient, type DatabaseAccountGetResults } from '@azure/arm-cosmosdb'; +import { type DatabaseAccountListKeysResult } from '@azure/arm-cosmosdb/src/models'; +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import vscode from 'vscode'; +import { type Experience } from '../AzureDBExperiences'; +import { type CosmosDBCredential, type CosmosDBKeyCredential } from '../docdb/getCosmosClient'; +import { createCosmosDBManagementClient } from '../utils/azureClients'; +import { localize } from '../utils/localize'; +import { nonNullProp } from '../utils/nonNull'; +import { type CosmosAccountModel } from './CosmosAccountModel'; +import { CosmosAccountResourceItemBase } from './CosmosAccountResourceItemBase'; + +export class DocumentDBAccountResourceItem extends CosmosAccountResourceItemBase { + protected databaseAccount?: DatabaseAccountGetResults; + protected credentials?: CosmosDBCredential[]; + protected documentEndpoint?: string; + + constructor( + account: CosmosAccountModel, + protected experience: Experience, + ) { + super(account); + } + + protected getClient() { + return callWithTelemetryAndErrorHandling( + 'CosmosAccountResourceItemBase.getClient', + async (context: IActionContext) => { + return createCosmosDBManagementClient(context, this.account.subscription); + }, + ); + } + + protected async init(): Promise { + const id = nonNullProp(this.account, 'id'); + const name = nonNullProp(this.account, 'name'); + const resourceGroup = nonNullProp(this.account, 'resourceGroup'); + const client = await this.getClient(); + + if (!client) { + return; + } + + const databaseAccount = await client.databaseAccounts.get(resourceGroup, name); + this.credentials = await this.getCredentials(name, resourceGroup, client, databaseAccount); + this.documentEndpoint = nonNullProp(databaseAccount, 'documentEndpoint', `of the database account ${id}`); + } + + private async getCredentials( + name: string, + resourceGroup: string, + client: CosmosDBManagementClient, + databaseAccount: DatabaseAccountGetResults, + ): Promise { + const result = await callWithTelemetryAndErrorHandling( + 'CosmosDBBranchDataProvider.getCredentials', + async (context: IActionContext) => { + const localAuthDisabled = databaseAccount.disableLocalAuth === true; + const forceOAuth = vscode.workspace.getConfiguration().get('azureDatabases.useCosmosOAuth'); + context.telemetry.properties.useCosmosOAuth = (forceOAuth ?? false).toString(); + + let keyCred: CosmosDBKeyCredential | undefined = undefined; + // disable key auth if the user has opted in to OAuth (AAD/Entra ID) + if (!forceOAuth) { + try { + context.telemetry.properties.localAuthDisabled = localAuthDisabled.toString(); + + let keyResult: DatabaseAccountListKeysResult | undefined; + // If the account has local auth disabled, don't even try to use key auth + if (!localAuthDisabled) { + keyResult = await client.databaseAccounts.listKeys(resourceGroup, name); + keyCred = keyResult?.primaryMasterKey + ? { + type: 'key', + key: keyResult.primaryMasterKey, + } + : undefined; + context.telemetry.properties.receivedKeyCreds = 'true'; + } else { + throw new Error('Local auth is disabled'); + } + } catch { + context.telemetry.properties.receivedKeyCreds = 'false'; + const message = localize( + 'keyPermissionErrorMsg', + 'You do not have the required permissions to list auth keys for [{0}].\nFalling back to using Entra ID.\nYou can change the default authentication in the settings.', + name, + ); + const openSettingsItem = localize('openSettings', 'Open Settings'); + void vscode.window.showWarningMessage(message, ...[openSettingsItem]).then((item) => { + if (item === openSettingsItem) { + void vscode.commands.executeCommand( + 'workbench.action.openSettings', + 'azureDatabases.useCosmosOAuth', + ); + } + }); + } + } + + // OAuth is always enabled for Cosmos DB and will be used as a fallback if key auth is unavailable + const authCred = { type: 'auth' }; + return [keyCred, authCred].filter((cred): cred is CosmosDBCredential => cred !== undefined); + }, + ); + + return result ?? []; + } +} diff --git a/src/tree/graph/GraphAccountResourceItem.ts b/src/tree/graph/GraphAccountResourceItem.ts index 00f153118..484c31627 100644 --- a/src/tree/graph/GraphAccountResourceItem.ts +++ b/src/tree/graph/GraphAccountResourceItem.ts @@ -3,18 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type CosmosDBCredential } from '../../docdb/getCosmosClient'; -import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; -import { type GraphAccountModel } from './GraphAccountModel'; +import { tryGetGremlinEndpointFromAzure } from '../../graph/gremlinEndpoints'; +import { nonNullProp } from '../../utils/nonNull'; +import { type IGremlinEndpoint } from '../../vscode-cosmosdbgraph.api'; +import { DocumentDBAccountResourceItem } from '../DocumentDBAccountResourceItem'; -export class GraphAccountResourceItem extends CosmosAccountResourceItemBase { - constructor( - account: GraphAccountModel, - private readonly credentials: CosmosDBCredential[], - private readonly documentEndpoint: string, - private readonly gremlinEndpoint: string, - ) { - super(account); +export class GraphAccountResourceItem extends DocumentDBAccountResourceItem { + public gremlinEndpoint?: IGremlinEndpoint; + + protected override async init(): Promise { + await super.init(); + + const name = nonNullProp(this.account, 'name'); + const resourceGroup = nonNullProp(this.account, 'resourceGroup'); + const client = await this.getClient(); + + if (!client) { + return; + } + + this.gremlinEndpoint = await tryGetGremlinEndpointFromAzure(client, resourceGroup, name); } // here, we can add more methods or properties specific to MongoDB diff --git a/src/tree/mongo/DatabaseItem.ts b/src/tree/mongo/DatabaseItem.ts index e4c575b14..5a443ae0d 100644 --- a/src/tree/mongo/DatabaseItem.ts +++ b/src/tree/mongo/DatabaseItem.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createGenericElement, type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { createGenericElement } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; +import { type CosmosDbTreeElement } from '../CosmosDbTreeElement'; import { type IDatabaseInfo } from './IDatabaseInfo'; import { type MongoAccountModel } from './MongoAccountModel'; -export class DatabaseItem { +export class DatabaseItem implements CosmosDbTreeElement { id: string; constructor( @@ -18,7 +19,7 @@ export class DatabaseItem { this.id = `${account.id}/${databaseInfo.name}`; } - async getChildren(): Promise { + async getChildren(): Promise { return [ createGenericElement({ contextValue: 'mongoClusters.item.no-collection', @@ -26,7 +27,7 @@ export class DatabaseItem { label: 'Create collection...', commandId: 'command.mongoClusters.createCollection', commandArgs: [this], - }), + }) as CosmosDbTreeElement, ]; } // const client: MongoClustersClient = await MongoClustersClient.getClient(this.mongoCluster.id); diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index d9ffe7d17..2b1d547f0 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -10,7 +10,6 @@ import { nonNullProp, parseError, type IActionContext, - type TreeElementBase, } from '@microsoft/vscode-azext-utils'; import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; import { type MongoClient } from 'mongodb'; @@ -20,13 +19,16 @@ import { connectToMongoClient } from '../../mongo/connectToMongoClient'; import { getDatabaseNameFromConnectionString } from '../../mongo/mongoConnectionStrings'; import { createCosmosDBManagementClient } from '../../utils/azureClients'; import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; +import { type CosmosDbTreeElement } from '../CosmosDbTreeElement'; import { DatabaseItem } from './DatabaseItem'; import { type IDatabaseInfo } from './IDatabaseInfo'; import { type MongoAccountModel } from './MongoAccountModel'; export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { + protected declare account: MongoAccountModel; // Not adding a new property, just changing the type of an existing one + constructor( - protected account: MongoAccountModel, + account: MongoAccountModel, protected subscription?: AzureSubscription, // optional for the case of a workspace connection readonly databaseAccount?: DatabaseAccountGetResults, readonly isEmulator?: boolean, @@ -71,8 +73,8 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { return result ?? undefined; } - async getChildren(): Promise { - ext.outputChannel.appendLine(`Cosmos DB for MongoDB (RU): Loading details for "${this.cosmosAccount.name}"`); + async getChildren(): Promise { + ext.outputChannel.appendLine(`Cosmos DB for MongoDB (RU): Loading details for "${this.account.name}"`); let mongoClient: MongoClient | undefined; try { diff --git a/src/tree/nosql/NoSqlAccountResourceItem.ts b/src/tree/nosql/NoSqlAccountResourceItem.ts index 8a6bfc4a1..00c4ea96e 100644 --- a/src/tree/nosql/NoSqlAccountResourceItem.ts +++ b/src/tree/nosql/NoSqlAccountResourceItem.ts @@ -3,21 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type CosmosDBCredential } from '../../docdb/getCosmosClient'; -import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; -import { type NoSqlAccountModel } from './NoSqlAccountModel'; +import { DocumentDBAccountResourceItem } from '../DocumentDBAccountResourceItem'; -export class NoSqlAccountResourceItem extends CosmosAccountResourceItemBase { - constructor( - account: NoSqlAccountModel, - private readonly credentials: CosmosDBCredential[], - private readonly documentEndpoint: string, - ) { - super(account); - // - // // Default to DocumentDB, the base type for all Cosmos DB Accounts - // return new DocDBAccountTreeItem(parent, id, label, documentEndpoint, credentials, isEmulator, databaseAccount); - } - - // here, we can add more methods or properties specific to MongoDB -} +export class NoSqlAccountResourceItem extends DocumentDBAccountResourceItem {} diff --git a/src/tree/table/TableAccountResourceItem.ts b/src/tree/table/TableAccountResourceItem.ts index e72804e97..245123c0b 100644 --- a/src/tree/table/TableAccountResourceItem.ts +++ b/src/tree/table/TableAccountResourceItem.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type CosmosDBCredential } from '../../docdb/getCosmosClient'; -import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; -import { type TableAccountModel } from './TableAccountModel'; +import { createGenericElement } from '@microsoft/vscode-azext-utils'; +import { type CosmosDbTreeElement } from '../CosmosDbTreeElement'; +import { DocumentDBAccountResourceItem } from '../DocumentDBAccountResourceItem'; -export class TableAccountResourceItem extends CosmosAccountResourceItemBase { - constructor( - account: TableAccountModel, - private readonly credentials: CosmosDBCredential[], - private readonly documentEndpoint: string, - ) { - super(account); +export class TableAccountResourceItem extends DocumentDBAccountResourceItem { + public getChildren(): Promise { + return Promise.resolve([ + createGenericElement({ + contextValue: 'tableNotSupported', + label: 'Table Accounts are not supported yet.', + id: `${this.id}/no-databases`, + }) as CosmosDbTreeElement, + ]); } - - // here, we can add more methods or properties specific to MongoDB } From 9c71293dc7a1569224d2df230603c5d91955e42b Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 17 Dec 2024 16:32:09 +0100 Subject: [PATCH 07/42] integrating with 'provider' changes during API unification --- src/AzureDBExperiences.ts | 2 +- src/mongoClusters/tree/DatabaseItem.ts | 9 +- src/tree/CosmosAccountResourceItemBase.ts | 3 +- src/tree/CosmosDBBranchDataProvider.ts | 3 +- src/tree/CosmosDbTreeElement.ts | 2 +- src/tree/graph/GraphAccountModel.ts | 2 +- src/tree/mongo/DatabaseItem.ts | 63 ------- src/tree/mongo/IDatabaseInfo.ts | 9 - src/tree/mongo/MongoAccountModel.ts | 5 +- src/tree/mongo/MongoAccountResourceItem.ts | 198 ++++++++++----------- 10 files changed, 112 insertions(+), 184 deletions(-) delete mode 100644 src/tree/mongo/DatabaseItem.ts delete mode 100644 src/tree/mongo/IDatabaseInfo.ts diff --git a/src/AzureDBExperiences.ts b/src/AzureDBExperiences.ts index b545a748b..47fb0da8f 100644 --- a/src/AzureDBExperiences.ts +++ b/src/AzureDBExperiences.ts @@ -166,7 +166,7 @@ export const MongoClustersExprience: Experience = { api: API.MongoClusters, longName: 'Cosmos DB for MongoDB (vCore)', shortName: 'MongoDB (vCore)', - telemetryName: 'mongoClusters' + telemetryName: 'mongoClusters', } as const; export const TableExperience: Experience = { api: API.Table, diff --git a/src/mongoClusters/tree/DatabaseItem.ts b/src/mongoClusters/tree/DatabaseItem.ts index 12c85180d..59ff842b1 100644 --- a/src/mongoClusters/tree/DatabaseItem.ts +++ b/src/mongoClusters/tree/DatabaseItem.ts @@ -3,7 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createGenericElement, type IActionContext, type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { + createGenericElement, + type IActionContext, + type TreeElementBase, + type TreeElementWithId, +} from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; import { ext } from '../../extensionVariables'; @@ -12,7 +17,7 @@ import { MongoClustersClient, type DatabaseItemModel } from '../MongoClustersCli import { CollectionItem } from './CollectionItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class DatabaseItem { +export class DatabaseItem implements TreeElementWithId { id: string; constructor( diff --git a/src/tree/CosmosAccountResourceItemBase.ts b/src/tree/CosmosAccountResourceItemBase.ts index 8cadd1ce1..80c53b1b8 100644 --- a/src/tree/CosmosAccountResourceItemBase.ts +++ b/src/tree/CosmosAccountResourceItemBase.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { type TreeElementBase } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import { type TreeItem } from 'vscode'; import { getExperienceLabel, tryGetExperience } from '../AzureDBExperiences'; @@ -20,7 +21,7 @@ export abstract class CosmosAccountResourceItemBase implements CosmosDbTreeEleme * Returns the children of the cluster. * @returns The children of the cluster. */ - getChildren(): Promise { + getChildren(): Promise { return Promise.resolve([]); } diff --git a/src/tree/CosmosDBBranchDataProvider.ts b/src/tree/CosmosDBBranchDataProvider.ts index 150b3056e..7a1685d65 100644 --- a/src/tree/CosmosDBBranchDataProvider.ts +++ b/src/tree/CosmosDBBranchDataProvider.ts @@ -14,7 +14,6 @@ import { type CosmosAccountModel, type CosmosDBResource } from './CosmosAccountM import { type CosmosDbTreeElement } from './CosmosDbTreeElement'; import { DocumentDBAccountResourceItem } from './DocumentDBAccountResourceItem'; import { GraphAccountResourceItem } from './graph/GraphAccountResourceItem'; -import { type MongoAccountModel } from './mongo/MongoAccountModel'; import { MongoAccountResourceItem } from './mongo/MongoAccountResourceItem'; import { NoSqlAccountResourceItem } from './nosql/NoSqlAccountResourceItem'; import { TableAccountResourceItem } from './table/TableAccountResourceItem'; @@ -75,7 +74,7 @@ export class CosmosDBBranchDataProvider const experience = tryGetExperience(resource); if (experience?.api === API.MongoDB) { - return new MongoAccountResourceItem(accountModel as MongoAccountModel, resource.subscription); + return new MongoAccountResourceItem(accountModel, experience); } if (experience?.api === API.Cassandra) { diff --git a/src/tree/CosmosDbTreeElement.ts b/src/tree/CosmosDbTreeElement.ts index aa1f84516..526bdc0f1 100644 --- a/src/tree/CosmosDbTreeElement.ts +++ b/src/tree/CosmosDbTreeElement.ts @@ -11,4 +11,4 @@ export interface ExtTreeElementBase extends TreeElementWithId { getTreeItem(): vscode.TreeItem | Thenable; } -export type CosmosDbTreeElement = ExtTreeElementBase; +export type CosmosDbTreeElement = TreeElementWithId; diff --git a/src/tree/graph/GraphAccountModel.ts b/src/tree/graph/GraphAccountModel.ts index 050e7f504..3038f55b3 100644 --- a/src/tree/graph/GraphAccountModel.ts +++ b/src/tree/graph/GraphAccountModel.ts @@ -3,6 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type CosmosAccountModel } from '../CosmosAccountModel'; +import { type CosmosAccountModel } from '../CosmosAccountModel'; export type GraphAccountModel = CosmosAccountModel; diff --git a/src/tree/mongo/DatabaseItem.ts b/src/tree/mongo/DatabaseItem.ts deleted file mode 100644 index 5a443ae0d..000000000 --- a/src/tree/mongo/DatabaseItem.ts +++ /dev/null @@ -1,63 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createGenericElement } from '@microsoft/vscode-azext-utils'; -import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type CosmosDbTreeElement } from '../CosmosDbTreeElement'; -import { type IDatabaseInfo } from './IDatabaseInfo'; -import { type MongoAccountModel } from './MongoAccountModel'; - -export class DatabaseItem implements CosmosDbTreeElement { - id: string; - - constructor( - readonly account: MongoAccountModel, - readonly databaseInfo: IDatabaseInfo, - ) { - this.id = `${account.id}/${databaseInfo.name}`; - } - - async getChildren(): Promise { - return [ - createGenericElement({ - contextValue: 'mongoClusters.item.no-collection', - id: `${this.id}/no-databases`, - label: 'Create collection...', - commandId: 'command.mongoClusters.createCollection', - commandArgs: [this], - }) as CosmosDbTreeElement, - ]; - } - // const client: MongoClustersClient = await MongoClustersClient.getClient(this.mongoCluster.id); - // const collections = await client.listCollections(this.databaseInfo.name); - - // if (collections.length === 0) { - // // no databases in there: - // return [ - // createGenericElement({ - // contextValue: 'mongoClusters.item.no-collection', - // id: `${this.id}/no-databases`, - // label: 'Create collection...', - // iconPath: new vscode.ThemeIcon('plus'), - // commandId: 'command.mongoClusters.createCollection', - // commandArgs: [this], - // }), - // ]; - // } - - // return collections.map((collection) => { - // return new CollectionItem(this.mongoCluster, this.databaseInfo, collection); - // }); - - getTreeItem(): TreeItem { - return { - id: this.id, - contextValue: 'mongoClusters.item.database', - label: this.databaseInfo.name, - iconPath: new ThemeIcon('database'), - collapsibleState: TreeItemCollapsibleState.Collapsed, - }; - } -} diff --git a/src/tree/mongo/IDatabaseInfo.ts b/src/tree/mongo/IDatabaseInfo.ts deleted file mode 100644 index 810b4b60a..000000000 --- a/src/tree/mongo/IDatabaseInfo.ts +++ /dev/null @@ -1,9 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export interface IDatabaseInfo { - name: string; - empty?: boolean; -} diff --git a/src/tree/mongo/MongoAccountModel.ts b/src/tree/mongo/MongoAccountModel.ts index 3530e3f89..8451d85ea 100644 --- a/src/tree/mongo/MongoAccountModel.ts +++ b/src/tree/mongo/MongoAccountModel.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type MongoClusterModel } from '../../mongoClusters/tree/MongoClusterModel'; import { type CosmosAccountModel } from '../CosmosAccountModel'; -export type MongoAccountModel = CosmosAccountModel & MongoClusterModel; +export type MongoAccountModel = CosmosAccountModel & { + connectionString?: string; +}; diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index 80244df8c..aba011e68 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -7,49 +7,45 @@ import { type DatabaseAccountGetResults } from '@azure/arm-cosmosdb'; import { callWithTelemetryAndErrorHandling, nonNullProp, - parseError, type IActionContext, + type TreeElementWithId, } from '@microsoft/vscode-azext-utils'; import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; -import { type MongoClient } from 'mongodb'; import ConnectionString from 'mongodb-connection-string-url'; -import { Links } from '../../constants'; +import { type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; -import { getDatabaseNameFromConnectionString } from '../../mongo/mongoConnectionStrings'; import { CredentialCache } from '../../mongoClusters/CredentialCache'; import { MongoClustersClient, type DatabaseItemModel } from '../../mongoClusters/MongoClustersClient'; import { DatabaseItem } from '../../mongoClusters/tree/DatabaseItem'; import { type MongoClusterModel } from '../../mongoClusters/tree/MongoClusterModel'; import { createCosmosDBManagementClient } from '../../utils/azureClients'; import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; -import { type CosmosDbTreeElement } from '../CosmosDbTreeElement'; -import { DatabaseItem } from './DatabaseItem'; -import { type IDatabaseInfo } from './IDatabaseInfo'; import { type MongoAccountModel } from './MongoAccountModel'; +/** + * This implementation relies on information from the CosmosAccountModel, i.e. + * will only behave as expected when used in the context of an Azure Subscription. + */ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { - protected declare account: MongoAccountModel; // Not adding a new property, just changing the type of an existing one + protected declare account: MongoAccountModel; constructor( account: MongoAccountModel, - protected subscription?: AzureSubscription, // available when the account is a azure-resource one + protected experience: Experience, readonly databaseAccount?: DatabaseAccountGetResults, // TODO: exploring during v1->v2 migration readonly isEmulator?: boolean, // TODO: exploring during v1->v2 migration ) { super(account); } - async discoverConnectionString(): Promise { + async discoverConnectionStringFromAccountInfo(): Promise { const result = await callWithTelemetryAndErrorHandling( - 'cosmosDB.mongo.authenticate', + 'cosmosDB.mongo.discoverConnectionString', async (context: IActionContext) => { - ext.outputChannel.appendLine( - `Cosmos DB for MongoDB (RU): Attempting to authenticate with "${this.account.name}"...`, - ); // Create a client to interact with the MongoDB vCore management API and read the cluster details const managementClient = await createCosmosDBManagementClient( context, - this.subscription as AzureSubscription, + this.account.subscription as AzureSubscription, ); const connectionStringsInfo = await managementClient.databaseAccounts.listConnectionStrings( this.account.resourceGroup as string, @@ -59,6 +55,7 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { const connectionString: URL = new URL( nonNullProp(nonNullProp(connectionStringsInfo, 'connectionStrings')[0], 'connectionString'), ); + // for any Mongo connectionString, append this query param because the Cosmos Mongo API v3.6 doesn't support retrywrites // but the newer node.js drivers started breaking this const searchParam: string = 'retrywrites'; @@ -76,56 +73,30 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { return result ?? undefined; } - async getChildren(): Promise { - ext.outputChannel.appendLine(`Cosmos DB for MongoDB (RU): Loading details for "${this.account.name}"`); - - let mongoClient: MongoClient | undefined; - try { - let databases: DatabaseItemModel[]; + async getChildren(): Promise { + let mongoClient: MongoClustersClient | null; + + // Check if credentials are cached, and return the cached client if available + if (CredentialCache.hasCredentials(this.id)) { + ext.outputChannel.appendLine( + `${this.experience.longName}: Reusing active connection details for "${this.account.name}".`, + ); + mongoClient = await MongoClustersClient.getClient(this.id); + } else { + ext.outputChannel.appendLine( + `${this.experience.longName}: Activating connection for "${this.account.name}"`, + ); + + if (this.account.subscription) { + const cString = await this.discoverConnectionStringFromAccountInfo(); + this.account.connectionString = cString; + } if (!this.account.connectionString) { - if (this.subscription) { - const cString = await this.discoverConnectionString(); - if (!cString) { - throw new Error('Failed to discover the connection string.'); - } - this.account.connectionString = cString; - } - if (!this.account.connectionString) { - throw new Error('Missing connection string'); - } + throw new Error('Connection string not found.'); } - let mongoClient: MongoClustersClient | null; - - // Check if credentials are cached, and return the cached client if available - if (CredentialCache.hasCredentials(this.id)) { - ext.outputChannel.appendLine(`MongoDB (RU): Reusing active connection for "${this.account.name}".`); - mongoClient = await MongoClustersClient.getClient(this.id); - } else { - // Call to the abstract method to authenticate and connect to the cluster - const cString = new ConnectionString(this.account.connectionString); - const username: string | undefined = cString.username; - const password: string | undefined = cString.password; - CredentialCache.setCredentials(this.id, cString.toString(), username, password); - - try { - mongoClient = await MongoClustersClient.getClient(this.id).catch((error: Error) => { - ext.outputChannel.appendLine('failed.'); - ext.outputChannel.appendLine(`Error: ${error.message}`); - - throw error; - }); - } catch (error) { - console.error(error); - // If connection fails, remove cached credentials - await MongoClustersClient.deleteClient(this.id); - CredentialCache.deleteCredentials(this.id); - - // Return null to indicate failure - return []; - } - } + const cString = new ConnectionString(this.account.connectionString); // // Azure MongoDB accounts need to have the name passed in for private endpoints // mongoClient = await connectToMongoClient( @@ -133,47 +104,70 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { // this.databaseAccount ? nonNullProp(this.databaseAccount, 'name') : appendExtensionUserAgent(), // ); - const databaseInConnectionString = getDatabaseNameFromConnectionString(this.account.connectionString); - if (databaseInConnectionString && !this.isEmulator) { - // emulator violates the connection string format - // If the database is in the connection string, that's all we connect to (we might not even have permissions to list databases) - databases = [ - { - name: databaseInConnectionString, - empty: false, - }, - ]; - } else { - // https://mongodb.github.io/node-mongodb-native/3.1/api/index.html - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - databases = await mongoClient.listDatabases(); - } - return databases - .filter( - (databaseInfo: IDatabaseInfo) => - !(databaseInfo.name && databaseInfo.name.toLowerCase() === 'admin' && databaseInfo.empty), - ) // Filter out the 'admin' database if it's empty - .map((database) => { - const clusterInfo = this.account as MongoClusterModel; - // eslint-disable-next-line no-unused-vars - const databaseInfo: DatabaseItemModel = { - name: database.name, - empty: database.empty, - }; - - return new DatabaseItem(clusterInfo, databaseInfo); - }); - } catch (error) { - const message = parseError(error).message; - if (this.isEmulator && message.includes('ECONNREFUSED')) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - error.message = `Unable to reach emulator. See ${Links.LocalConnectionDebuggingTips} for debugging tips.\n${message}`; - } - throw error; - } finally { - if (mongoClient) { - void mongoClient.close(); - } + //TODO: simplify the api for CrednetialCache to accept full connection strings with credentials + const username: string | undefined = cString.username; + const password: string | undefined = cString.password; + CredentialCache.setCredentials(this.id, cString.toString(), username, password); + + mongoClient = await MongoClustersClient.getClient(this.id).catch(async (error) => { + console.error(error); + // If connection fails, remove cached credentials, as they might be invalid + await MongoClustersClient.deleteClient(this.id); + CredentialCache.deleteCredentials(this.id); + return null; + }); } + + if (!mongoClient) { + throw new Error('Failed to connect.'); + } + + // TODO: add support for single databases via connection string. + // move it to monogoclustersclient + // + // const databaseInConnectionString = getDatabaseNameFromConnectionString(this.account.connectionString); + // if (databaseInConnectionString && !this.isEmulator) { + // // emulator violates the connection string format + // // If the database is in the connection string, that's all we connect to (we might not even have permissions to list databases) + // databases = [ + // { + // name: databaseInConnectionString, + // empty: false, + // }, + // ]; + // } + + // https://mongodb.github.io/node-mongodb-native/3.1/api/index.html + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const databases = await mongoClient.listDatabases(); + + return databases + .filter( + (databaseInfo) => + !(databaseInfo.name && databaseInfo.name.toLowerCase() === 'admin' && databaseInfo.empty), + ) // Filter out the 'admin' database if it's empty + .map((database) => { + const clusterInfo = this.account as MongoClusterModel; + // eslint-disable-next-line no-unused-vars + const databaseInfo: DatabaseItemModel = { + name: database.name, + empty: database.empty, + }; + + return new DatabaseItem(clusterInfo, databaseInfo); + }); + + // } catch (error) { + // const message = parseError(error).message; + // if (this.isEmulator && message.includes('ECONNREFUSED')) { + // // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + // error.message = `Unable to reach emulator. See ${Links.LocalConnectionDebuggingTips} for debugging tips.\n${message}`; + // } + // throw error; + // } finally { + // if (mongoClient) { + // void mongoClient.close(); + // } + // } } } From fca14b122b48c2a49651ef4fb0298e09ee24cfcf Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 18 Dec 2024 11:28:06 +0100 Subject: [PATCH 08/42] expanding contextValue for mongo* workloads --- package.json | 16 ++++----- src/mongoClusters/MongoClustersClient.ts | 5 ++- .../commands/createCollection.ts | 2 ++ src/mongoClusters/commands/createDatabase.ts | 2 ++ src/mongoClusters/commands/dropCollection.ts | 2 ++ src/mongoClusters/commands/dropDatabase.ts | 2 ++ src/mongoClusters/commands/exportDocuments.ts | 4 +++ src/mongoClusters/commands/importDocuments.ts | 2 ++ src/mongoClusters/commands/launchShell.ts | 4 ++- src/mongoClusters/tree/CollectionItem.ts | 11 ++++-- src/mongoClusters/tree/DatabaseItem.ts | 8 +++-- src/mongoClusters/tree/IndexItem.ts | 4 +-- src/mongoClusters/tree/IndexesItem.ts | 4 +-- .../tree/MongoClusterItemBase.ts | 14 ++++++-- src/mongoClusters/tree/MongoClusterModel.ts | 4 +-- .../workspace/MongoClusterWorkspaceItem.ts | 3 +- .../workspace/MongoDBAccountsWorkspaceItem.ts | 6 ++-- src/tree/mongo/MongoAccountResourceItem.ts | 34 ++++++++----------- 18 files changed, 79 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index b7bb7f1ee..ddfce2798 100644 --- a/package.json +++ b/package.json @@ -1143,38 +1143,38 @@ }, { "command": "command.mongoClusters.dropCollection", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem == mongoClusters.item.collection" + "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i" }, { "command": "command.mongoClusters.dropDatabase", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem == mongoClusters.item.database" + "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i" }, { "command": "command.mongoClusters.removeWorkspaceConnection", - "when": "vscodeDatabases.mongoClustersSupportEnabled && view == azureWorkspace && viewItem == mongoClusters.item.mongoCluster" + "when": "vscodeDatabases.mongoClustersSupportEnabled && view == azureWorkspace && viewItem =~ /treeitem.mongoCluster/i && viewItem =~ /(mongocluster|mongodb)/i" }, { "command": "command.mongoClusters.createCollection", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem == mongoClusters.item.database" + "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i" }, { "command": "command.mongoClusters.createDatabase", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /mongoClusters.item.mongoCluster/i", + "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.mongoCluster/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "1@1" }, { "command": "command.mongoClusters.importDocuments", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem == mongoClusters.item.collection", + "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "1@1" }, { "command": "command.mongoClusters.exportDocuments", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem == mongoClusters.item.collection", + "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "1@2" }, { "command": "command.mongoClusters.launchShell", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /mongoClusters.item.(mongoCluster|database|collection)/i", + "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.(mongoCluster|database|collection)/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "2@1" } ], diff --git a/src/mongoClusters/MongoClustersClient.ts b/src/mongoClusters/MongoClustersClient.ts index 3df82ca9e..453b45138 100644 --- a/src/mongoClusters/MongoClustersClient.ts +++ b/src/mongoClusters/MongoClustersClient.ts @@ -132,7 +132,10 @@ export class MongoClustersClient { async listDatabases(): Promise { const rawDatabases: ListDatabasesResult = await this._mongoClient.db().admin().listDatabases(); - const databases: DatabaseItemModel[] = rawDatabases.databases; + const databases: DatabaseItemModel[] = rawDatabases.databases.filter( + // Filter out the 'admin' database if it's empty + (databaseInfo) => !(databaseInfo.name && databaseInfo.name.toLowerCase() === 'admin' && databaseInfo.empty), + ); return databases; } diff --git a/src/mongoClusters/commands/createCollection.ts b/src/mongoClusters/commands/createCollection.ts index 59d5fa123..7f60acf83 100644 --- a/src/mongoClusters/commands/createCollection.ts +++ b/src/mongoClusters/commands/createCollection.ts @@ -11,6 +11,8 @@ import { type CreateCollectionWizardContext } from '../wizards/create/createWiza import { CollectionNameStep } from '../wizards/create/PromptCollectionNameStep'; export async function createCollection(context: IActionContext, databaseNode?: DatabaseItem): Promise { + context.telemetry.properties.experience = databaseNode?.mongoCluster.dbExperience?.api; + // node ??= ... pick a node if not provided if (!databaseNode) { throw new Error('No database selected.'); diff --git a/src/mongoClusters/commands/createDatabase.ts b/src/mongoClusters/commands/createDatabase.ts index a949e2c29..72f49626e 100644 --- a/src/mongoClusters/commands/createDatabase.ts +++ b/src/mongoClusters/commands/createDatabase.ts @@ -15,6 +15,8 @@ import { import { DatabaseNameStep } from '../wizards/create/PromptDatabaseNameStep'; export async function createDatabase(context: IActionContext, clusterNode?: MongoClusterResourceItem): Promise { + context.telemetry.properties.experience = clusterNode?.mongoCluster.dbExperience?.api; + // node ??= ... pick a node if not provided if (!clusterNode) { throw new Error('No cluster selected.'); diff --git a/src/mongoClusters/commands/dropCollection.ts b/src/mongoClusters/commands/dropCollection.ts index 9499829ca..1773fa4ed 100644 --- a/src/mongoClusters/commands/dropCollection.ts +++ b/src/mongoClusters/commands/dropCollection.ts @@ -10,6 +10,8 @@ import { localize } from '../../utils/localize'; import { type CollectionItem } from '../tree/CollectionItem'; export async function dropCollection(context: IActionContext, node?: CollectionItem): Promise { + context.telemetry.properties.experience = node?.mongoCluster.dbExperience?.api; + // node ??= ... pick a node if not provided if (!node) { throw new Error('No collection selected.'); diff --git a/src/mongoClusters/commands/dropDatabase.ts b/src/mongoClusters/commands/dropDatabase.ts index 52f3b860b..62856a105 100644 --- a/src/mongoClusters/commands/dropDatabase.ts +++ b/src/mongoClusters/commands/dropDatabase.ts @@ -10,6 +10,8 @@ import { localize } from '../../utils/localize'; import { type DatabaseItem } from '../tree/DatabaseItem'; export async function dropDatabase(context: IActionContext, node?: DatabaseItem): Promise { + context.telemetry.properties.experience = node?.mongoCluster.dbExperience?.api; + // node ??= ... pick a node if not provided if (!node) { throw new Error('No database selected.'); diff --git a/src/mongoClusters/commands/exportDocuments.ts b/src/mongoClusters/commands/exportDocuments.ts index f885da3b9..f166a1b2a 100644 --- a/src/mongoClusters/commands/exportDocuments.ts +++ b/src/mongoClusters/commands/exportDocuments.ts @@ -13,6 +13,8 @@ import { MongoClustersClient } from '../MongoClustersClient'; import { type CollectionItem } from '../tree/CollectionItem'; export async function mongoClustersExportEntireCollection(context: IActionContext, node?: CollectionItem) { + context.telemetry.properties.experience = node?.mongoCluster.dbExperience?.api; + return mongoClustersExportQueryResults(context, node); } @@ -21,6 +23,8 @@ export async function mongoClustersExportQueryResults( node?: CollectionItem, props?: { queryText?: string; source?: string }, ): Promise { + context.telemetry.properties.experience = node?.mongoCluster.dbExperience?.api; + // node ??= ... pick a node if not provided if (!node) { throw new Error('No collection selected.'); diff --git a/src/mongoClusters/commands/importDocuments.ts b/src/mongoClusters/commands/importDocuments.ts index 5b4250867..4e3d0c948 100644 --- a/src/mongoClusters/commands/importDocuments.ts +++ b/src/mongoClusters/commands/importDocuments.ts @@ -13,6 +13,8 @@ export async function mongoClustersImportDocuments( _collectionNodes?: CollectionItem[], // required by the TreeNodeCommandCallback, but not used ...args: unknown[] ): Promise { + context.telemetry.properties.experience = collectionNode?.mongoCluster.dbExperience?.api; + const source = (args[0] as { source?: string })?.source || 'contextMenu'; context.telemetry.properties.calledFrom = source; diff --git a/src/mongoClusters/commands/launchShell.ts b/src/mongoClusters/commands/launchShell.ts index 92e999369..b73d070c6 100644 --- a/src/mongoClusters/commands/launchShell.ts +++ b/src/mongoClusters/commands/launchShell.ts @@ -16,9 +16,11 @@ import { } from '../utils/connectionStringHelpers'; export async function launchShell( - _context: IActionContext, + context: IActionContext, node?: DatabaseItem | CollectionItem | MongoClusterResourceItem, ): Promise { + context.telemetry.properties.experience = node?.mongoCluster.dbExperience?.api; + if (!node) { throw new Error('No database or collection selected.'); } diff --git a/src/mongoClusters/tree/CollectionItem.ts b/src/mongoClusters/tree/CollectionItem.ts index 672ed39dd..40586d9d5 100644 --- a/src/mongoClusters/tree/CollectionItem.ts +++ b/src/mongoClusters/tree/CollectionItem.ts @@ -3,7 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createGenericElement, type IActionContext, type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { + createContextValue, + createGenericElement, + type IActionContext, + type TreeElementBase, +} from '@microsoft/vscode-azext-utils'; import { type Document } from 'bson'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; import { ext } from '../../extensionVariables'; @@ -30,7 +35,7 @@ export class CollectionItem { async getChildren(): Promise { return [ createGenericElement({ - contextValue: 'mongoClusters.item.documents', + contextValue: createContextValue(['treeitem.documents', this.mongoCluster.dbExperience?.api ?? '']), id: `${this.id}/documents`, label: 'Documents', commandId: 'command.internal.mongoClusters.containerView.open', @@ -80,7 +85,7 @@ export class CollectionItem { getTreeItem(): TreeItem { return { id: this.id, - contextValue: 'mongoClusters.item.collection', + contextValue: createContextValue(['treeitem.collection', this.mongoCluster.dbExperience?.api ?? '']), label: this.collectionInfo.name, iconPath: new ThemeIcon('folder-opened'), collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/DatabaseItem.ts b/src/mongoClusters/tree/DatabaseItem.ts index 59ff842b1..c47eb9e73 100644 --- a/src/mongoClusters/tree/DatabaseItem.ts +++ b/src/mongoClusters/tree/DatabaseItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { + createContextValue, createGenericElement, type IActionContext, type TreeElementBase, @@ -35,7 +36,10 @@ export class DatabaseItem implements TreeElementWithId { // no databases in there: return [ createGenericElement({ - contextValue: 'mongoClusters.item.no-collection', + contextValue: createContextValue([ + 'treeitem.no-collections', + this.mongoCluster.dbExperience?.api ?? '', + ]), id: `${this.id}/no-databases`, label: 'Create collection...', iconPath: new vscode.ThemeIcon('plus'), @@ -82,7 +86,7 @@ export class DatabaseItem implements TreeElementWithId { getTreeItem(): TreeItem { return { id: this.id, - contextValue: 'mongoClusters.item.database', + contextValue: createContextValue(['treeitem.database', this.mongoCluster.dbExperience?.api ?? '']), label: this.databaseInfo.name, iconPath: new ThemeIcon('database'), // TODO: create our onw icon here, this one's shape can change collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/IndexItem.ts b/src/mongoClusters/tree/IndexItem.ts index b42fb5f73..bbb2de0bf 100644 --- a/src/mongoClusters/tree/IndexItem.ts +++ b/src/mongoClusters/tree/IndexItem.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createGenericElement, type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { createContextValue, createGenericElement, type TreeElementBase } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; import { type CollectionItemModel, type DatabaseItemModel, type IndexItemModel } from '../MongoClustersClient'; import { type MongoClusterModel } from './MongoClusterModel'; @@ -38,7 +38,7 @@ export class IndexItem { getTreeItem(): TreeItem { return { id: this.id, - contextValue: 'mongoClusters.item.index', + contextValue: createContextValue(['treeitem.index', this.mongoCluster.dbExperience?.api ?? '']), label: this.indexInfo.name, iconPath: new ThemeIcon('combine'), // TODO: create our onw icon here, this one's shape can change collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/IndexesItem.ts b/src/mongoClusters/tree/IndexesItem.ts index deb059f89..9f021c483 100644 --- a/src/mongoClusters/tree/IndexesItem.ts +++ b/src/mongoClusters/tree/IndexesItem.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { createContextValue, type TreeElementBase } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; import { MongoClustersClient, type CollectionItemModel, type DatabaseItemModel } from '../MongoClustersClient'; import { IndexItem } from './IndexItem'; @@ -31,7 +31,7 @@ export class IndexesItem { getTreeItem(): TreeItem { return { id: this.id, - contextValue: 'mongoClusters.item.indexes', + contextValue: createContextValue(['treeitem.indexes', this.mongoCluster.dbExperience?.api ?? '']), label: 'Indexes', iconPath: new ThemeIcon('combine'), // TODO: create our onw icon here, this one's shape can change collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/MongoClusterItemBase.ts b/src/mongoClusters/tree/MongoClusterItemBase.ts index 701123fbc..13ecf527d 100644 --- a/src/mongoClusters/tree/MongoClusterItemBase.ts +++ b/src/mongoClusters/tree/MongoClusterItemBase.ts @@ -3,7 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createGenericElement, type IActionContext, type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { + createContextValue, + createGenericElement, + type IActionContext, + type TreeElementBase, +} from '@microsoft/vscode-azext-utils'; import { type TreeItem } from 'vscode'; import * as vscode from 'vscode'; @@ -79,7 +84,10 @@ export abstract class MongoClusterItemBase implements TreeElementBase { if (databases.length === 0) { return [ createGenericElement({ - contextValue: 'mongoClusters.item.no-databases', + contextValue: createContextValue([ + 'treeitem.no-databases', + this.mongoCluster.dbExperience?.api ?? '', + ]), id: `${this.id}/no-databases`, label: 'Create database...', iconPath: new vscode.ThemeIcon('plus'), @@ -123,7 +131,7 @@ export abstract class MongoClusterItemBase implements TreeElementBase { getTreeItem(): TreeItem { return { id: this.id, - contextValue: 'mongoClusters.item.mongoCluster', + contextValue: createContextValue(['treeitem.mongocluster', this.mongoCluster.dbExperience?.api ?? '']), label: this.mongoCluster.name, description: this.mongoCluster.sku !== undefined ? `(${this.mongoCluster.sku})` : false, // iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg'), // Uncomment if icon is available diff --git a/src/mongoClusters/tree/MongoClusterModel.ts b/src/mongoClusters/tree/MongoClusterModel.ts index c2acf3906..da39ffb93 100644 --- a/src/mongoClusters/tree/MongoClusterModel.ts +++ b/src/mongoClusters/tree/MongoClusterModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { type MongoCluster, type Resource } from '@azure/arm-cosmosdb'; -import { type API } from '../../AzureDBExperiences'; +import { type Experience } from '../../AzureDBExperiences'; // Selecting only the properties used in the extension, but keeping an easy option to extend the model later and offer full coverage of MongoCluster // '|' means that you can only access properties that are common to both types. @@ -35,7 +35,7 @@ interface ResourceModelInUse extends Resource { resourceGroup?: string; // adding support for MongoRU and vCore - dbExperience?: API.MongoDB | API.MongoClusters; + dbExperience?: Experience; isServerless?: boolean; } diff --git a/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts index b64f7844b..358a64441 100644 --- a/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts @@ -6,6 +6,7 @@ import { AzureWizard, callWithTelemetryAndErrorHandling, + createContextValue, nonNullProp, nonNullValue, UserCancelledError, @@ -153,7 +154,7 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase { getTreeItem(): vscode.TreeItem { return { id: this.id, - contextValue: 'mongoClusters.item.mongoCluster', + contextValue: createContextValue(['treeitem.mongocluster', this.mongoCluster.dbExperience?.api ?? '']), label: this.mongoCluster.name, description: this.mongoCluster.sku !== undefined ? `(${this.mongoCluster.sku})` : false, iconPath: new vscode.ThemeIcon('server-environment'), // Uncomment if icon is available diff --git a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts index 3f6876761..81aad0bb8 100644 --- a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts @@ -5,7 +5,7 @@ import { createGenericElement, type TreeElementBase } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { API } from '../../../AzureDBExperiences'; +import { MongoClustersExprience } from '../../../AzureDBExperiences'; import { WorkspaceResourceType } from '../../../tree/workspace/sharedWorkspaceResourceProvider'; import { SharedWorkspaceStorage } from '../../../tree/workspace/sharedWorkspaceStorage'; import { type MongoClusterModel } from '../MongoClusterModel'; @@ -26,13 +26,13 @@ export class MongoDBAccountsWorkspaceItem implements TreeElementBase { const model: MongoClusterModel = { id: item.id, name: item.name, - dbExperience: API.MongoClusters, + dbExperience: MongoClustersExprience, connectionString: item?.secrets?.[0] ?? undefined, }; return new MongoClusterWorkspaceItem(model); }), createGenericElement({ - contextValue: this.id + '/newConnection', + contextValue: 'treeitem.newConnection', id: this.id + '/newConnection', label: 'New Connection...', iconPath: new ThemeIcon('plus'), diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index aba011e68..b5c3d2eed 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -23,7 +23,7 @@ import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase' import { type MongoAccountModel } from './MongoAccountModel'; /** - * This implementation relies on information from the CosmosAccountModel, i.e. + * This implementation relies on information from the MongoAccountModel, i.e. * will only behave as expected when used in the context of an Azure Subscription. */ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { @@ -122,8 +122,7 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { throw new Error('Failed to connect.'); } - // TODO: add support for single databases via connection string. - // move it to monogoclustersclient + // TODO: add support for single databases via connection string. move it to monogoclustersclient // // const databaseInConnectionString = getDatabaseNameFromConnectionString(this.account.connectionString); // if (databaseInConnectionString && !this.isEmulator) { @@ -137,25 +136,20 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { // ]; // } - // https://mongodb.github.io/node-mongodb-native/3.1/api/index.html - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const databases = await mongoClient.listDatabases(); - return databases - .filter( - (databaseInfo) => - !(databaseInfo.name && databaseInfo.name.toLowerCase() === 'admin' && databaseInfo.empty), - ) // Filter out the 'admin' database if it's empty - .map((database) => { - const clusterInfo = this.account as MongoClusterModel; - // eslint-disable-next-line no-unused-vars - const databaseInfo: DatabaseItemModel = { - name: database.name, - empty: database.empty, - }; - - return new DatabaseItem(clusterInfo, databaseInfo); - }); + return databases.map((database) => { + const clusterInfo = this.account as MongoClusterModel; + clusterInfo.dbExperience = this.experience; + + // eslint-disable-next-line no-unused-vars + const databaseInfo: DatabaseItemModel = { + name: database.name, + empty: database.empty, + }; + + return new DatabaseItem(clusterInfo, databaseInfo); + }); // } catch (error) { // const message = parseError(error).message; From f69ed3558c34ae1b1008b61a3a4a069bea932a01 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 7 Jan 2025 14:51:31 +0100 Subject: [PATCH 09/42] typo --- src/mongoClusters/tree/DatabaseItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mongoClusters/tree/DatabaseItem.ts b/src/mongoClusters/tree/DatabaseItem.ts index c47eb9e73..0f4913a5b 100644 --- a/src/mongoClusters/tree/DatabaseItem.ts +++ b/src/mongoClusters/tree/DatabaseItem.ts @@ -88,7 +88,7 @@ export class DatabaseItem implements TreeElementWithId { id: this.id, contextValue: createContextValue(['treeitem.database', this.mongoCluster.dbExperience?.api ?? '']), label: this.databaseInfo.name, - iconPath: new ThemeIcon('database'), // TODO: create our onw icon here, this one's shape can change + iconPath: new ThemeIcon('database'), // TODO: create our own icon here, this one's shape can change collapsibleState: TreeItemCollapsibleState.Collapsed, }; } From fea71e51892945db3065c015a4170f4f4c813be1 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 7 Jan 2025 15:09:49 +0100 Subject: [PATCH 10/42] build fix --- src/mongoClusters/tree/MongoClustersBranchDataProvider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts index 3e6d9df38..5b3bfa920 100644 --- a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts +++ b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts @@ -12,7 +12,7 @@ import { type ResourceModelBase, } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; -import { API } from '../../AzureDBExperiences'; +import { API, MongoClustersExprience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; import { createMongoClustersManagementClient } from '../../utils/azureClients'; import { type MongoClusterModel } from './MongoClusterModel'; @@ -142,7 +142,7 @@ export class MongoClustersBranchDataProvider accounts.map((MongoClustersAccount) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.detailsCache.set(nonNullProp(MongoClustersAccount, 'id'), { - dbExperience: API.MongoClusters, + dbExperience: MongoClustersExprience, id: MongoClustersAccount.id as string, name: MongoClustersAccount.name as string, resourceGroup: getResourceGroupFromId(MongoClustersAccount.id as string), From 98dcec2f833f42c960cee68f3b15bec478ecb682 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 7 Jan 2025 15:44:47 +0100 Subject: [PATCH 11/42] vCore: added "create document..." menu item --- package.json | 10 +++++++ src/mongoClusters/MongoClustersExtension.ts | 3 +++ src/mongoClusters/commands/createDocument.ts | 26 +++++++++++++++++++ .../commands/openCollectionView.ts | 5 ++-- .../commands/openDocumentView.ts | 4 +-- src/mongoClusters/tree/CollectionItem.ts | 2 +- .../collectionViewController.ts | 2 ++ .../collectionView/collectionViewRouter.ts | 7 ++--- .../documentView/documentsViewController.ts | 4 +-- .../documentView/documentsViewRouter.ts | 9 +++---- 10 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 src/mongoClusters/commands/createDocument.ts diff --git a/package.json b/package.json index ddfce2798..ac9dd99f5 100644 --- a/package.json +++ b/package.json @@ -624,6 +624,11 @@ "command": "command.mongoClusters.exportDocuments", "title": "Export Documents from Collection..." }, + { + "category": "MongoDB Clusters", + "command": "command.mongoClusters.createDocument", + "title": "Create Document..." + }, { "category": "MongoDB Clusters", "command": "command.mongoClusters.launchShell", @@ -1172,6 +1177,11 @@ "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "1@2" }, + { + "command": "command.mongoClusters.createDocument", + "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "1@1" + }, { "command": "command.mongoClusters.launchShell", "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.(mongoCluster|database|collection)/i && viewItem =~ /(mongocluster|mongodb)/i", diff --git a/src/mongoClusters/MongoClustersExtension.ts b/src/mongoClusters/MongoClustersExtension.ts index 3156e9094..0959316a2 100644 --- a/src/mongoClusters/MongoClustersExtension.ts +++ b/src/mongoClusters/MongoClustersExtension.ts @@ -25,6 +25,7 @@ import { import { addWorkspaceConnection } from './commands/addWorkspaceConnection'; import { createCollection } from './commands/createCollection'; import { createDatabase } from './commands/createDatabase'; +import { createDocument } from './commands/createDocument'; import { dropCollection } from './commands/dropCollection'; import { dropDatabase } from './commands/dropDatabase'; import { mongoClustersExportEntireCollection, mongoClustersExportQueryResults } from './commands/exportDocuments'; @@ -94,6 +95,8 @@ export class MongoClustersExtension implements vscode.Disposable { registerCommandWithTreeNodeUnwrapping('command.mongoClusters.createCollection', createCollection); registerCommandWithTreeNodeUnwrapping('command.mongoClusters.createDatabase', createDatabase); + registerCommandWithTreeNodeUnwrapping('command.mongoClusters.createDocument', createDocument); + registerCommandWithTreeNodeUnwrapping( 'command.mongoClusters.importDocuments', mongoClustersImportDocuments, diff --git a/src/mongoClusters/commands/createDocument.ts b/src/mongoClusters/commands/createDocument.ts new file mode 100644 index 000000000..dee28f9fe --- /dev/null +++ b/src/mongoClusters/commands/createDocument.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type CollectionItem } from '../tree/CollectionItem'; + +import * as vscode from 'vscode'; + + +export async function createDocument(context: IActionContext, node?: CollectionItem): Promise { + context.telemetry.properties.experience = node?.mongoCluster.dbExperience?.api; + + // node ??= ... pick a node if not provided + if (!node) { + throw new Error('No collection selected.'); + } + + await vscode.commands.executeCommand('command.internal.mongoClusters.documentView.open', { + clusterId: node.mongoCluster.id, + databaseName: node.databaseInfo.name, + collectionName: node.collectionInfo.name, + mode: 'add', + }); +} diff --git a/src/mongoClusters/commands/openCollectionView.ts b/src/mongoClusters/commands/openCollectionView.ts index 560fdd0dc..7fbbf4c9a 100644 --- a/src/mongoClusters/commands/openCollectionView.ts +++ b/src/mongoClusters/commands/openCollectionView.ts @@ -12,7 +12,7 @@ export async function openCollectionView( _context: IActionContext, props: { id: string; - liveConnectionId: string; + clusterId: string; databaseName: string; collectionName: string; collectionTreeItem: CollectionItem; @@ -22,12 +22,13 @@ export async function openCollectionView( * We're starting a new "session" using the existing connection. * A session can cache data, handle paging, and convert data. */ - const sessionId = await MongoClustersSession.initNewSession(props.liveConnectionId); + const sessionId = await MongoClustersSession.initNewSession(props.clusterId); const view = new CollectionViewController({ id: props.id, sessionId: sessionId, + clusterId: props.clusterId, databaseName: props.databaseName, collectionName: props.collectionName, collectionTreeItem: props.collectionTreeItem, diff --git a/src/mongoClusters/commands/openDocumentView.ts b/src/mongoClusters/commands/openDocumentView.ts index 65d6b5b86..2824b5859 100644 --- a/src/mongoClusters/commands/openDocumentView.ts +++ b/src/mongoClusters/commands/openDocumentView.ts @@ -12,7 +12,7 @@ export function openDocumentView( props: { id: string; - sessionId: string; + clusterId: string; databaseName: string; collectionName: string; documentId: string; @@ -23,7 +23,7 @@ export function openDocumentView( const view = new DocumentsViewController({ id: props.id, - sessionId: props.sessionId, + clusterId: props.clusterId, databaseName: props.databaseName, collectionName: props.collectionName, documentId: props.documentId, diff --git a/src/mongoClusters/tree/CollectionItem.ts b/src/mongoClusters/tree/CollectionItem.ts index 40586d9d5..c830e34b8 100644 --- a/src/mongoClusters/tree/CollectionItem.ts +++ b/src/mongoClusters/tree/CollectionItem.ts @@ -45,7 +45,7 @@ export class CollectionItem { viewTitle: `${this.collectionInfo.name}`, // viewTitle: `${this.mongoCluster.name}/${this.databaseInfo.name}/${this.collectionInfo.name}`, // using '/' as a separator to use VSCode's "title compression"(?) feature - liveConnectionId: this.mongoCluster.id, + clusterId: this.mongoCluster.id, databaseName: this.databaseInfo.name, collectionName: this.collectionInfo.name, collectionTreeItem: this, diff --git a/src/webviews/mongoClusters/collectionView/collectionViewController.ts b/src/webviews/mongoClusters/collectionView/collectionViewController.ts index ea7324e83..7b17208e8 100644 --- a/src/webviews/mongoClusters/collectionView/collectionViewController.ts +++ b/src/webviews/mongoClusters/collectionView/collectionViewController.ts @@ -13,6 +13,7 @@ export type CollectionViewWebviewConfigurationType = { id: string; // move to base type sessionId: string; + clusterId: string; databaseName: string; collectionName: string; collectionTreeItem: CollectionItem; // needed to execute commands on the collection as the tree APIv2 doesn't support id-based search for tree items. @@ -31,6 +32,7 @@ export class CollectionViewController extends WebviewController Date: Tue, 7 Jan 2025 15:46:23 +0100 Subject: [PATCH 12/42] vCore: removed old mongodb create document command --- package.json | 10 ---------- src/mongo/commands/createMongoDocument.ts | 17 ----------------- src/mongo/registerMongoCommands.ts | 2 -- 3 files changed, 29 deletions(-) delete mode 100644 src/mongo/commands/createMongoDocument.ts diff --git a/package.json b/package.json index ac9dd99f5..8483ad6c1 100644 --- a/package.json +++ b/package.json @@ -365,11 +365,6 @@ "command": "cosmosDB.createMongoDatabase", "title": "Create Database..." }, - { - "category": "MongoDB", - "command": "cosmosDB.createMongoDocument", - "title": "Create Document" - }, { "category": "Cosmos DB", "command": "cosmosDB.deleteAccount", @@ -761,11 +756,6 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBMongoServer(?![a-z])/i", "group": "1@1" }, - { - "command": "cosmosDB.createMongoDocument", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == MongoCollection", - "group": "1@1" - }, { "command": "cosmosDB.createMongoCollection", "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == mongoDb", diff --git a/src/mongo/commands/createMongoDocument.ts b/src/mongo/commands/createMongoDocument.ts deleted file mode 100644 index b00994c5a..000000000 --- a/src/mongo/commands/createMongoDocument.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as vscode from 'vscode'; -import { MongoCollectionTreeItem } from '../tree/MongoCollectionTreeItem'; -import { pickMongo } from './pickMongo'; - -export async function createMongoDocument(context: IActionContext, node?: MongoCollectionTreeItem): Promise { - if (!node) { - node = await pickMongo(context, MongoCollectionTreeItem.contextValue); - } - const documentNode = await node.createChild(context); - await vscode.commands.executeCommand('cosmosDB.openDocument', documentNode); -} diff --git a/src/mongo/registerMongoCommands.ts b/src/mongo/registerMongoCommands.ts index 88b2627de..390c00169 100644 --- a/src/mongo/registerMongoCommands.ts +++ b/src/mongo/registerMongoCommands.ts @@ -16,7 +16,6 @@ import { ext } from '../extensionVariables'; import { connectMongoDatabase, loadPersistedMongoDB } from './commands/connectMongoDatabase'; import { createMongoCollection } from './commands/createMongoCollection'; import { createMongoDatabase } from './commands/createMongoDatabase'; -import { createMongoDocument } from './commands/createMongoDocument'; import { createMongoSrapbook } from './commands/createMongoScrapbook'; import { deleteMongoCollection } from './commands/deleteMongoCollection'; import { deleteMongoDB } from './commands/deleteMongoDatabase'; @@ -69,7 +68,6 @@ export function registerMongoCommands(): void { // #region Collection command registerCommandWithTreeNodeUnwrapping('cosmosDB.openCollection', openMongoCollection); - registerCommandWithTreeNodeUnwrapping('cosmosDB.createMongoDocument', createMongoDocument); registerCommandWithTreeNodeUnwrapping('cosmosDB.deleteMongoCollection', deleteMongoCollection); // #endregion From 3be3615b48a3c7610e6f26c47e64bfdb7faafb40 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 7 Jan 2025 17:34:16 +0100 Subject: [PATCH 13/42] MongoDB: added azureDatabases.refresh_v2 for Tree APIv2 --- package.json | 16 ++++++++++++++++ src/extension.ts | 16 +++++++++++++++- src/mongoClusters/tree/CollectionItem.ts | 12 +++++++++++- src/mongoClusters/tree/DatabaseItem.ts | 3 +++ src/tree/CosmosDbTreeElement.ts | 5 ++++- 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8483ad6c1..84c14be27 100644 --- a/package.json +++ b/package.json @@ -276,6 +276,11 @@ "command": "azureDatabases.refresh", "title": "Refresh", "icon": "$(refresh)" + },{ + "category": "Azure Databases", + "command": "azureDatabases.refresh_v2", + "title": "Refresh", + "icon": "$(refresh)" }, { "category": "Azure Databases", @@ -1176,7 +1181,18 @@ "command": "command.mongoClusters.launchShell", "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.(mongoCluster|database|collection)/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "2@1" + }, + { + "command": "azureDatabases.refresh_v2", + "when" : "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "4@1" + }, + { + "command": "azureDatabases.refresh_v2", + "when" : "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "4@1" } + ], "explorer/context": [ { diff --git a/src/extension.ts b/src/extension.ts index 5e664d6b7..84cef2d1d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,7 +21,7 @@ import { type AzExtTreeItem, type AzureExtensionApi, type IActionContext, - type ITreeItemPickerContext, + type ITreeItemPickerContext } from '@microsoft/vscode-azext-utils'; import { AzExtResourceType, getAzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api'; import { platform } from 'os'; @@ -60,6 +60,7 @@ import { DatabaseWorkspaceProvider } from './resolver/DatabaseWorkspaceProvider' import { TableAccountTreeItem } from './table/tree/TableAccountTreeItem'; import { AttachedAccountSuffix } from './tree/AttachedAccountsTreeItem'; import { CosmosDBBranchDataProvider } from './tree/CosmosDBBranchDataProvider'; +import { type CosmosDbTreeElement } from './tree/CosmosDbTreeElement'; import { SubscriptionTreeItem } from './tree/SubscriptionTreeItem'; import { localize } from './utils/localize'; @@ -169,6 +170,19 @@ export async function activateInternal( }, ); + registerCommandWithTreeNodeUnwrapping( + 'azureDatabases.refresh_v2', + (context: IActionContext, node?: CosmosDbTreeElement) => { + if (node?.experience) { + context.telemetry.properties.experience = node.experience.api; + } + + if (node?.id) { + ext.state.notifyChildrenChanged(node.id); + } + }, + ); + registerCommandWithTreeNodeUnwrapping( 'azureDatabases.detachDatabaseAccount', async (actionContext: IActionContext & ITreeItemPickerContext, node?: AzExtTreeItem) => { diff --git a/src/mongoClusters/tree/CollectionItem.ts b/src/mongoClusters/tree/CollectionItem.ts index c830e34b8..272425d2a 100644 --- a/src/mongoClusters/tree/CollectionItem.ts +++ b/src/mongoClusters/tree/CollectionItem.ts @@ -8,9 +8,11 @@ import { createGenericElement, type IActionContext, type TreeElementBase, + type TreeElementWithId } from '@microsoft/vscode-azext-utils'; import { type Document } from 'bson'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; import { MongoClustersClient, @@ -21,8 +23,9 @@ import { import { IndexesItem } from './IndexesItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class CollectionItem { +export class CollectionItem implements TreeElementWithId { id: string; + experience?: Experience; constructor( readonly mongoCluster: MongoClusterModel, @@ -30,6 +33,7 @@ export class CollectionItem { readonly collectionInfo: CollectionItemModel, ) { this.id = `${mongoCluster.id}/${databaseInfo.name}/${collectionInfo.name}`; + this.experience = mongoCluster.dbExperience; } async getChildren(): Promise { @@ -54,6 +58,12 @@ export class CollectionItem { iconPath: new ThemeIcon('explorer-view-icon'), }), new IndexesItem(this.mongoCluster, this.databaseInfo, this.collectionInfo), + createGenericElement({ + contextValue: createContextValue(['treeitem.documents', this.mongoCluster.dbExperience?.api ?? '']), + id: `${this.id}/documents/asdf`, + label: 'aa' + new Date().toLocaleTimeString(), + iconPath: new ThemeIcon('explorer-view-icon'), + }), ]; } diff --git a/src/mongoClusters/tree/DatabaseItem.ts b/src/mongoClusters/tree/DatabaseItem.ts index 0f4913a5b..9cb31db5e 100644 --- a/src/mongoClusters/tree/DatabaseItem.ts +++ b/src/mongoClusters/tree/DatabaseItem.ts @@ -12,6 +12,7 @@ import { } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; import { localize } from '../../utils/localize'; import { MongoClustersClient, type DatabaseItemModel } from '../MongoClustersClient'; @@ -20,12 +21,14 @@ import { type MongoClusterModel } from './MongoClusterModel'; export class DatabaseItem implements TreeElementWithId { id: string; + experience?: Experience; constructor( readonly mongoCluster: MongoClusterModel, readonly databaseInfo: DatabaseItemModel, ) { this.id = `${mongoCluster.id}/${databaseInfo.name}`; + this.experience = mongoCluster.dbExperience; } async getChildren(): Promise { diff --git a/src/tree/CosmosDbTreeElement.ts b/src/tree/CosmosDbTreeElement.ts index 526bdc0f1..af3a319bc 100644 --- a/src/tree/CosmosDbTreeElement.ts +++ b/src/tree/CosmosDbTreeElement.ts @@ -5,10 +5,13 @@ import { type TreeElementWithId } from '@microsoft/vscode-azext-utils'; import type * as vscode from 'vscode'; +import { type Experience } from '../AzureDBExperiences'; export interface ExtTreeElementBase extends TreeElementWithId { getChildren?(): vscode.ProviderResult; getTreeItem(): vscode.TreeItem | Thenable; } -export type CosmosDbTreeElement = TreeElementWithId; +export type CosmosDbTreeElement = TreeElementWithId & { + experience?: Experience; +}; From 680fafcf317cfda655f208f0798748842c95681c Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 7 Jan 2025 17:44:31 +0100 Subject: [PATCH 14/42] removed obsolete test code --- src/mongoClusters/tree/CollectionItem.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/mongoClusters/tree/CollectionItem.ts b/src/mongoClusters/tree/CollectionItem.ts index 272425d2a..68c01c028 100644 --- a/src/mongoClusters/tree/CollectionItem.ts +++ b/src/mongoClusters/tree/CollectionItem.ts @@ -57,13 +57,7 @@ export class CollectionItem implements TreeElementWithId { ], iconPath: new ThemeIcon('explorer-view-icon'), }), - new IndexesItem(this.mongoCluster, this.databaseInfo, this.collectionInfo), - createGenericElement({ - contextValue: createContextValue(['treeitem.documents', this.mongoCluster.dbExperience?.api ?? '']), - id: `${this.id}/documents/asdf`, - label: 'aa' + new Date().toLocaleTimeString(), - iconPath: new ThemeIcon('explorer-view-icon'), - }), + new IndexesItem(this.mongoCluster, this.databaseInfo, this.collectionInfo) ]; } From 5758550689079e29575b8d0378729b8ee875ab02 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 8 Jan 2025 13:41:24 +0100 Subject: [PATCH 15/42] updated azuredatabases.refresh behavior to support v2 tree api --- package.json | 25 +++++++++---- src/extension.ts | 37 ++++++++++--------- src/mongoClusters/commands/createDocument.ts | 1 - src/mongoClusters/tree/CollectionItem.ts | 7 ++-- src/mongoClusters/tree/DatabaseItem.ts | 3 +- src/mongoClusters/tree/IndexItem.ts | 13 ++++++- src/mongoClusters/tree/IndexesItem.ts | 8 +++- .../tree/MongoClusterItemBase.ts | 7 +++- .../workspace/MongoDBAccountsWorkspaceItem.ts | 9 +++-- src/tree/CosmosDbTreeElement.ts | 5 +-- src/tree/TreeElementWithExperience.ts | 25 +++++++++++++ 11 files changed, 98 insertions(+), 42 deletions(-) create mode 100644 src/tree/TreeElementWithExperience.ts diff --git a/package.json b/package.json index 84c14be27..416c5f9b9 100644 --- a/package.json +++ b/package.json @@ -276,11 +276,6 @@ "command": "azureDatabases.refresh", "title": "Refresh", "icon": "$(refresh)" - },{ - "category": "Azure Databases", - "command": "azureDatabases.refresh_v2", - "title": "Refresh", - "icon": "$(refresh)" }, { "category": "Azure Databases", @@ -1183,16 +1178,30 @@ "group": "2@1" }, { - "command": "azureDatabases.refresh_v2", + "command": "azureDatabases.refresh", + "when" : "viewItem =~ /treeitem.mongocluster/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "4@1" + }, + { + "command": "azureDatabases.refresh", "when" : "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "4@1" }, { - "command": "azureDatabases.refresh_v2", + "command": "azureDatabases.refresh", "when" : "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "4@1" + }, + { + "command": "azureDatabases.refresh", + "when" : "viewItem =~ /treeitem.indexes/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "4@1" + }, + { + "command": "azureDatabases.refresh", + "when" : "viewItem =~ /treeitem.index/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "4@1" } - ], "explorer/context": [ { diff --git a/src/extension.ts b/src/extension.ts index 84cef2d1d..727b4523e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,6 +7,7 @@ import { registerAzureUtilsExtensionVariables } from '@microsoft/vscode-azext-azureutils'; import { + AzExtTreeItem, callWithTelemetryAndErrorHandling, createApiProvider, createAzExtLogOutputChannel, @@ -18,10 +19,9 @@ import { TreeElementStateManager, type apiUtils, type AzExtParentTreeItem, - type AzExtTreeItem, type AzureExtensionApi, type IActionContext, - type ITreeItemPickerContext + type ITreeItemPickerContext, } from '@microsoft/vscode-azext-utils'; import { AzExtResourceType, getAzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api'; import { platform } from 'os'; @@ -60,8 +60,8 @@ import { DatabaseWorkspaceProvider } from './resolver/DatabaseWorkspaceProvider' import { TableAccountTreeItem } from './table/tree/TableAccountTreeItem'; import { AttachedAccountSuffix } from './tree/AttachedAccountsTreeItem'; import { CosmosDBBranchDataProvider } from './tree/CosmosDBBranchDataProvider'; -import { type CosmosDbTreeElement } from './tree/CosmosDbTreeElement'; import { SubscriptionTreeItem } from './tree/SubscriptionTreeItem'; +import { isTreeElementWithExperience } from './tree/TreeElementWithExperience'; import { localize } from './utils/localize'; const cosmosDBTopLevelContextValues: string[] = [ @@ -161,24 +161,27 @@ export async function activateInternal( }); registerCommandWithTreeNodeUnwrapping( 'azureDatabases.refresh', - async (actionContext: IActionContext, node?: AzExtTreeItem) => { - if (node) { - await node.refresh(actionContext); - } else { - await ext.rgApi.appResourceTree.refresh(actionContext, node); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async (actionContext: IActionContext, node?: any) => { + if (node instanceof AzExtTreeItem) { + if (node) { + await node.refresh(actionContext); + } else { + await ext.rgApi.appResourceTree.refresh(actionContext, node); + } + + return; } - }, - ); - registerCommandWithTreeNodeUnwrapping( - 'azureDatabases.refresh_v2', - (context: IActionContext, node?: CosmosDbTreeElement) => { - if (node?.experience) { - context.telemetry.properties.experience = node.experience.api; + // the node is not an AzExtTreeItem, so we assume it's a TreeElementWithId, etc., based on the V2 of the Tree API from Azure-Resources + + if (isTreeElementWithExperience(node)) { + actionContext.telemetry.properties.experience = node.experience?.api; } - if (node?.id) { - ext.state.notifyChildrenChanged(node.id); + if (node && typeof node === 'object' && 'id' in node) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + ext.state.notifyChildrenChanged(node.id as string); } }, ); diff --git a/src/mongoClusters/commands/createDocument.ts b/src/mongoClusters/commands/createDocument.ts index dee28f9fe..5e994e1e1 100644 --- a/src/mongoClusters/commands/createDocument.ts +++ b/src/mongoClusters/commands/createDocument.ts @@ -8,7 +8,6 @@ import { type CollectionItem } from '../tree/CollectionItem'; import * as vscode from 'vscode'; - export async function createDocument(context: IActionContext, node?: CollectionItem): Promise { context.telemetry.properties.experience = node?.mongoCluster.dbExperience?.api; diff --git a/src/mongoClusters/tree/CollectionItem.ts b/src/mongoClusters/tree/CollectionItem.ts index 68c01c028..2da6162c0 100644 --- a/src/mongoClusters/tree/CollectionItem.ts +++ b/src/mongoClusters/tree/CollectionItem.ts @@ -8,12 +8,13 @@ import { createGenericElement, type IActionContext, type TreeElementBase, - type TreeElementWithId + type TreeElementWithId, } from '@microsoft/vscode-azext-utils'; import { type Document } from 'bson'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; +import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { MongoClustersClient, type CollectionItemModel, @@ -23,7 +24,7 @@ import { import { IndexesItem } from './IndexesItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class CollectionItem implements TreeElementWithId { +export class CollectionItem implements TreeElementWithId, TreeElementWithExperience { id: string; experience?: Experience; @@ -57,7 +58,7 @@ export class CollectionItem implements TreeElementWithId { ], iconPath: new ThemeIcon('explorer-view-icon'), }), - new IndexesItem(this.mongoCluster, this.databaseInfo, this.collectionInfo) + new IndexesItem(this.mongoCluster, this.databaseInfo, this.collectionInfo), ]; } diff --git a/src/mongoClusters/tree/DatabaseItem.ts b/src/mongoClusters/tree/DatabaseItem.ts index 9cb31db5e..b56c3052a 100644 --- a/src/mongoClusters/tree/DatabaseItem.ts +++ b/src/mongoClusters/tree/DatabaseItem.ts @@ -14,12 +14,13 @@ import * as vscode from 'vscode'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; +import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { localize } from '../../utils/localize'; import { MongoClustersClient, type DatabaseItemModel } from '../MongoClustersClient'; import { CollectionItem } from './CollectionItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class DatabaseItem implements TreeElementWithId { +export class DatabaseItem implements TreeElementWithId, TreeElementWithExperience { id: string; experience?: Experience; diff --git a/src/mongoClusters/tree/IndexItem.ts b/src/mongoClusters/tree/IndexItem.ts index bbb2de0bf..c15e3486e 100644 --- a/src/mongoClusters/tree/IndexItem.ts +++ b/src/mongoClusters/tree/IndexItem.ts @@ -3,13 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createContextValue, createGenericElement, type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { + createContextValue, + createGenericElement, + type TreeElementBase, + type TreeElementWithId, +} from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { type CollectionItemModel, type DatabaseItemModel, type IndexItemModel } from '../MongoClustersClient'; import { type MongoClusterModel } from './MongoClusterModel'; -export class IndexItem { +export class IndexItem implements TreeElementWithId, TreeElementWithExperience { id: string; + experience?: Experience; constructor( readonly mongoCluster: MongoClusterModel, @@ -18,6 +26,7 @@ export class IndexItem { readonly indexInfo: IndexItemModel, ) { this.id = `${mongoCluster.id}/${databaseInfo.name}/${collectionInfo.name}/indexes/${indexInfo.name}`; + this.experience = mongoCluster.dbExperience; } async getChildren(): Promise { diff --git a/src/mongoClusters/tree/IndexesItem.ts b/src/mongoClusters/tree/IndexesItem.ts index 9f021c483..efa7575b0 100644 --- a/src/mongoClusters/tree/IndexesItem.ts +++ b/src/mongoClusters/tree/IndexesItem.ts @@ -3,14 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createContextValue, type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { createContextValue, type TreeElementBase, type TreeElementWithId } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { MongoClustersClient, type CollectionItemModel, type DatabaseItemModel } from '../MongoClustersClient'; import { IndexItem } from './IndexItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class IndexesItem { +export class IndexesItem implements TreeElementWithId, TreeElementWithExperience { id: string; + experience?: Experience; constructor( readonly mongoCluster: MongoClusterModel, @@ -18,6 +21,7 @@ export class IndexesItem { readonly collectionInfo: CollectionItemModel, ) { this.id = `${mongoCluster.id}/${databaseInfo.name}/${collectionInfo.name}/indexes`; + this.experience = mongoCluster.dbExperience; } async getChildren(): Promise { diff --git a/src/mongoClusters/tree/MongoClusterItemBase.ts b/src/mongoClusters/tree/MongoClusterItemBase.ts index 13ecf527d..167948cfb 100644 --- a/src/mongoClusters/tree/MongoClusterItemBase.ts +++ b/src/mongoClusters/tree/MongoClusterItemBase.ts @@ -8,11 +8,14 @@ import { createGenericElement, type IActionContext, type TreeElementBase, + type TreeElementWithId, } from '@microsoft/vscode-azext-utils'; import { type TreeItem } from 'vscode'; import * as vscode from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; +import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { localize } from '../../utils/localize'; import { regionToDisplayName } from '../../utils/regionToDisplayName'; import { CredentialCache } from '../CredentialCache'; @@ -21,11 +24,13 @@ import { DatabaseItem } from './DatabaseItem'; import { type MongoClusterModel } from './MongoClusterModel'; // This info will be available at every level in the tree for immediate access -export abstract class MongoClusterItemBase implements TreeElementBase { +export abstract class MongoClusterItemBase implements TreeElementWithId, TreeElementWithExperience { id: string; + experience?: Experience; constructor(public mongoCluster: MongoClusterModel) { this.id = mongoCluster.id ?? ''; + this.experience = mongoCluster.dbExperience; } /** diff --git a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts index 81aad0bb8..9d7ef68e1 100644 --- a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts @@ -3,19 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createGenericElement, type TreeElementBase } from '@microsoft/vscode-azext-utils'; +import { createGenericElement, type TreeElementBase, type TreeElementWithId } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { MongoClustersExprience } from '../../../AzureDBExperiences'; +import { MongoClustersExprience, type Experience } from '../../../AzureDBExperiences'; +import { type TreeElementWithExperience } from '../../../tree/TreeElementWithExperience'; import { WorkspaceResourceType } from '../../../tree/workspace/sharedWorkspaceResourceProvider'; import { SharedWorkspaceStorage } from '../../../tree/workspace/sharedWorkspaceStorage'; import { type MongoClusterModel } from '../MongoClusterModel'; import { MongoClusterWorkspaceItem } from './MongoClusterWorkspaceItem'; -export class MongoDBAccountsWorkspaceItem implements TreeElementBase { +export class MongoDBAccountsWorkspaceItem implements TreeElementWithId, TreeElementWithExperience { id: string; + experience?: Experience; constructor() { this.id = `vscode.cosmosdb.workspace.mongoclusters.accounts`; + this.experience = MongoClustersExprience; } async getChildren(): Promise { diff --git a/src/tree/CosmosDbTreeElement.ts b/src/tree/CosmosDbTreeElement.ts index af3a319bc..526bdc0f1 100644 --- a/src/tree/CosmosDbTreeElement.ts +++ b/src/tree/CosmosDbTreeElement.ts @@ -5,13 +5,10 @@ import { type TreeElementWithId } from '@microsoft/vscode-azext-utils'; import type * as vscode from 'vscode'; -import { type Experience } from '../AzureDBExperiences'; export interface ExtTreeElementBase extends TreeElementWithId { getChildren?(): vscode.ProviderResult; getTreeItem(): vscode.TreeItem | Thenable; } -export type CosmosDbTreeElement = TreeElementWithId & { - experience?: Experience; -}; +export type CosmosDbTreeElement = TreeElementWithId; diff --git a/src/tree/TreeElementWithExperience.ts b/src/tree/TreeElementWithExperience.ts new file mode 100644 index 000000000..3f04962f4 --- /dev/null +++ b/src/tree/TreeElementWithExperience.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Experience } from '../AzureDBExperiences'; + +/** + * It's currently being kept separately from the CosmosDbTreeElement as we need to discuss it with the team, + * as we're working on an overlapping feature in parallel, keeping the 'experience' property in a separate + * interface simplifies parallel development and can still be easily merged once ready for it. + */ +export type TreeElementWithExperience = { + experience?: Experience; // optional during the migration phase +}; + +/** + * Type guard function to check if a given node is a `TreeElementWithExperience`. + * + * @param node - The node to check. + * @returns `true` if the node is an object and has an `experience` property, otherwise `false`. + */ +export function isTreeElementWithExperience(node: unknown): node is TreeElementWithExperience { + return typeof node === 'object' && node !== null && 'experience' in node; +} From 38a4854e57eec5bd1eef4ab096b1963af4c7abde Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 8 Jan 2025 13:50:00 +0100 Subject: [PATCH 16/42] build fixes --- src/extension.ts | 4 ++-- src/mongoClusters/tree/MongoClustersBranchDataProvider.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 727b4523e..8d0be40fc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -228,9 +228,9 @@ export async function activateInternal( registerCommandWithTreeNodeUnwrapping('cosmosDB.copyConnectionString', cosmosDBCopyConnectionString); registerCommandWithTreeNodeUnwrapping( 'cosmosDB.openDocument', - async (actionContext: IActionContext, node?: MongoDocumentTreeItem | DocDBDocumentTreeItem) => { + async (actionContext: IActionContext, node?: DocDBDocumentTreeItem) => { if (!node) { - node = await ext.rgApi.pickAppResource( + node = await ext.rgApi.pickAppResource( actionContext, { filter: [cosmosMongoFilter, sqlFilter], diff --git a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts index 5b3bfa920..af07e6d0d 100644 --- a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts +++ b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts @@ -95,7 +95,7 @@ export class MongoClustersBranchDataProvider // 1. extract the basic info from the element (subscription, resource group, etc., provided by Azure Resources) let clusterInfo: MongoClusterModel = element as MongoClusterModel; - clusterInfo.dbExperience = API.MongoClusters; + clusterInfo.dbExperience = MongoClustersExprience; // 2. lookup the details in the cache, on subsequent refreshes, the details will be available in the cache if (this.detailsCache.has(clusterInfo.id)) { From 4e84fde2e224e24b221a7518d67c3cf459af00a3 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 8 Jan 2025 14:16:21 +0100 Subject: [PATCH 17/42] MongoDB: V1->V2, removed unused commands, deleted and commented out obsolete code, wip --- src/commands/api/DatabaseTreeItemInternal.ts | 5 +-- src/commands/api/findTreeItem.ts | 18 ++++----- src/commands/api/pickTreeItem.ts | 26 ++++++------- src/commands/importDocuments.ts | 6 +-- src/extension.ts | 20 ++++------ src/mongo/commands/createMongoCollection.ts | 17 --------- src/mongo/commands/createMongoDatabase.ts | 20 ---------- src/mongo/commands/deleteMongoCollection.ts | 17 --------- src/mongo/commands/deleteMongoDatabase.ts | 31 ---------------- src/mongo/commands/deleteMongoDocument.ts | 17 --------- src/mongo/commands/openMongoCollection.ts | 16 -------- src/mongo/registerMongoCommands.ts | 27 -------------- src/mongo/tree/MongoAccountTreeItem.ts | 23 ++++++------ src/mongo/tree/MongoCollectionTreeItem.ts | 18 ++++----- src/mongo/tree/MongoDocumentTreeItem.ts | 39 +++++++------------- src/tree/CosmosDBBranchDataProvider.ts | 8 +++- 16 files changed, 73 insertions(+), 235 deletions(-) delete mode 100644 src/mongo/commands/createMongoCollection.ts delete mode 100644 src/mongo/commands/createMongoDatabase.ts delete mode 100644 src/mongo/commands/deleteMongoCollection.ts delete mode 100644 src/mongo/commands/deleteMongoDatabase.ts delete mode 100644 src/mongo/commands/deleteMongoDocument.ts delete mode 100644 src/mongo/commands/openMongoCollection.ts diff --git a/src/commands/api/DatabaseTreeItemInternal.ts b/src/commands/api/DatabaseTreeItemInternal.ts index d104bdf45..649ab1d6e 100644 --- a/src/commands/api/DatabaseTreeItemInternal.ts +++ b/src/commands/api/DatabaseTreeItemInternal.ts @@ -12,7 +12,6 @@ import { type DocDBAccountTreeItemBase } from '../../docdb/tree/DocDBAccountTree import { type DocDBDatabaseTreeItemBase } from '../../docdb/tree/DocDBDatabaseTreeItemBase'; import { ext } from '../../extensionVariables'; import { type MongoAccountTreeItem } from '../../mongo/tree/MongoAccountTreeItem'; -import { type MongoDatabaseTreeItem } from '../../mongo/tree/MongoDatabaseTreeItem'; import { type ParsedConnectionString } from '../../ParsedConnectionString'; import { type PostgresDatabaseTreeItem } from '../../postgres/tree/PostgresDatabaseTreeItem'; import { type PostgresServerTreeItem } from '../../postgres/tree/PostgresServerTreeItem'; @@ -26,8 +25,8 @@ export class DatabaseTreeItemInternal extends DatabaseAccountTreeItemInternal im constructor( parsedCS: ParsedConnectionString, databaseName: string, - accountNode?: MongoAccountTreeItem | DocDBAccountTreeItemBase | PostgresServerTreeItem, - dbNode?: MongoDatabaseTreeItem | DocDBDatabaseTreeItemBase | PostgresDatabaseTreeItem, + accountNode?: DocDBAccountTreeItemBase | PostgresServerTreeItem, + dbNode?: DocDBDatabaseTreeItemBase | PostgresDatabaseTreeItem, ) { super(parsedCS, accountNode); this.databaseName = databaseName; diff --git a/src/commands/api/findTreeItem.ts b/src/commands/api/findTreeItem.ts index ec2624d15..76be41ee3 100644 --- a/src/commands/api/findTreeItem.ts +++ b/src/commands/api/findTreeItem.ts @@ -13,8 +13,6 @@ import { DocDBAccountTreeItemBase } from '../../docdb/tree/DocDBAccountTreeItemB import { DocDBDatabaseTreeItemBase } from '../../docdb/tree/DocDBDatabaseTreeItemBase'; import { ext } from '../../extensionVariables'; import { parseMongoConnectionString } from '../../mongo/mongoConnectionStrings'; -import { MongoAccountTreeItem } from '../../mongo/tree/MongoAccountTreeItem'; -import { MongoDatabaseTreeItem } from '../../mongo/tree/MongoDatabaseTreeItem'; import { type ParsedConnectionString } from '../../ParsedConnectionString'; import { createPostgresConnectionString, @@ -29,6 +27,10 @@ import { cacheTreeItem, tryGetTreeItemFromCache } from './apiCache'; import { DatabaseAccountTreeItemInternal } from './DatabaseAccountTreeItemInternal'; import { DatabaseTreeItemInternal } from './DatabaseTreeItemInternal'; +/** + * TODO: This needs a rewrite to match v2 + */ + export async function findTreeItem( query: TreeItemQuery, ): Promise { @@ -115,9 +117,10 @@ async function searchDbAccounts( } let actual: ParsedConnectionString; - if (dbAccount instanceof MongoAccountTreeItem) { - actual = await parseMongoConnectionString(dbAccount.connectionString); - } else if (dbAccount instanceof DocDBAccountTreeItemBase) { + // if (dbAccount instanceof MongoAccountTreeItem) { + // actual = await parseMongoConnectionString(dbAccount.connectionString); + // } else + if (dbAccount instanceof DocDBAccountTreeItemBase) { actual = parseDocDBConnectionString(dbAccount.connectionString); } else if (dbAccount instanceof PostgresServerTreeItem) { actual = dbAccount.partialConnectionString; @@ -129,10 +132,7 @@ async function searchDbAccounts( if (expected.databaseName) { const dbs = await dbAccount.getCachedChildren(context); for (const db of dbs) { - if ( - (db instanceof MongoDatabaseTreeItem || db instanceof DocDBDatabaseTreeItemBase) && - expected.databaseName === db.databaseName - ) { + if (db instanceof DocDBDatabaseTreeItemBase && expected.databaseName === db.databaseName) { return new DatabaseTreeItemInternal(expected, expected.databaseName, dbAccount, db); } if ( diff --git a/src/commands/api/pickTreeItem.ts b/src/commands/api/pickTreeItem.ts index 3abf8d7b4..266942af2 100644 --- a/src/commands/api/pickTreeItem.ts +++ b/src/commands/api/pickTreeItem.ts @@ -12,9 +12,7 @@ import { DocDBDatabaseTreeItem } from '../../docdb/tree/DocDBDatabaseTreeItem'; import { DocDBDatabaseTreeItemBase } from '../../docdb/tree/DocDBDatabaseTreeItemBase'; import { ext } from '../../extensionVariables'; import { GraphDatabaseTreeItem } from '../../graph/tree/GraphDatabaseTreeItem'; -import { parseMongoConnectionString } from '../../mongo/mongoConnectionStrings'; -import { MongoAccountTreeItem } from '../../mongo/tree/MongoAccountTreeItem'; -import { MongoDatabaseTreeItem } from '../../mongo/tree/MongoDatabaseTreeItem'; +import { type MongoAccountTreeItem } from '../../mongo/tree/MongoAccountTreeItem'; import { type ParsedConnectionString } from '../../ParsedConnectionString'; import { PostgresDatabaseTreeItem } from '../../postgres/tree/PostgresDatabaseTreeItem'; import { PostgresServerTreeItem } from '../../postgres/tree/PostgresServerTreeItem'; @@ -29,16 +27,17 @@ import { cacheTreeItem } from './apiCache'; import { DatabaseAccountTreeItemInternal } from './DatabaseAccountTreeItemInternal'; import { DatabaseTreeItemInternal } from './DatabaseTreeItemInternal'; +/** + * TODO: This needs a rewrite to match v2 + */ + const databaseContextValues = [ - MongoDatabaseTreeItem.contextValue, DocDBDatabaseTreeItem.contextValue, GraphDatabaseTreeItem.contextValue, PostgresDatabaseTreeItem.contextValue, ]; function getDatabaseContextValue(apiType: AzureDatabasesApiType): string { switch (apiType) { - case 'Mongo': - return MongoDatabaseTreeItem.contextValue; case 'SQL': return DocDBDatabaseTreeItem.contextValue; case 'Graph': @@ -76,20 +75,17 @@ export async function pickTreeItem( let parsedCS: ParsedConnectionString; let accountNode: MongoAccountTreeItem | DocDBAccountTreeItemBase | PostgresServerTreeItem; - let databaseNode: MongoDatabaseTreeItem | DocDBDatabaseTreeItemBase | PostgresDatabaseTreeItem | undefined; - if (pickedItem instanceof MongoAccountTreeItem) { - parsedCS = await parseMongoConnectionString(pickedItem.connectionString); - accountNode = pickedItem; - } else if (pickedItem instanceof DocDBAccountTreeItemBase) { + let databaseNode: DocDBDatabaseTreeItemBase | PostgresDatabaseTreeItem | undefined; + // if (pickedItem instanceof MongoAccountTreeItem) { + // parsedCS = await parseMongoConnectionString(pickedItem.connectionString); + // accountNode = pickedItem; + // } else + if (pickedItem instanceof DocDBAccountTreeItemBase) { parsedCS = parseDocDBConnectionString(pickedItem.connectionString); accountNode = pickedItem; } else if (pickedItem instanceof PostgresServerTreeItem) { parsedCS = await pickedItem.getFullConnectionString(); accountNode = pickedItem; - } else if (pickedItem instanceof MongoDatabaseTreeItem) { - parsedCS = await parseMongoConnectionString(pickedItem.connectionString); - accountNode = pickedItem.parent; - databaseNode = pickedItem; } else if (pickedItem instanceof DocDBDatabaseTreeItemBase) { parsedCS = parseDocDBConnectionString(pickedItem.connectionString); accountNode = pickedItem.parent; diff --git a/src/commands/importDocuments.ts b/src/commands/importDocuments.ts index 9ee006e94..108418158 100644 --- a/src/commands/importDocuments.ts +++ b/src/commands/importDocuments.ts @@ -19,7 +19,7 @@ import { getRootPath } from '../utils/workspacUtils'; export async function importDocuments( context: IActionContext, uris: vscode.Uri[] | undefined, - collectionNode: MongoCollectionTreeItem | DocDBCollectionTreeItem | CollectionItem | undefined, + collectionNode: DocDBCollectionTreeItem | CollectionItem | undefined, ): Promise { if (!uris) { uris = await askForDocuments(context); @@ -39,9 +39,9 @@ export async function importDocuments( ext.outputChannel.show(); } if (!collectionNode) { - collectionNode = await ext.rgApi.pickAppResource(context, { + collectionNode = await ext.rgApi.pickAppResource(context, { filter: [cosmosMongoFilter, sqlFilter], - expectedChildContextValue: [MongoCollectionTreeItem.contextValue, DocDBCollectionTreeItem.contextValue], + expectedChildContextValue: [DocDBCollectionTreeItem.contextValue], }); } diff --git a/src/extension.ts b/src/extension.ts index 8d0be40fc..545443237 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -51,7 +51,6 @@ import { GraphAccountTreeItem } from './graph/tree/GraphAccountTreeItem'; import { registerMongoCommands } from './mongo/registerMongoCommands'; import { setConnectedNode } from './mongo/setConnectedNode'; import { MongoAccountTreeItem } from './mongo/tree/MongoAccountTreeItem'; -import { type MongoCollectionTreeItem } from './mongo/tree/MongoCollectionTreeItem'; import { MongoDocumentTreeItem } from './mongo/tree/MongoDocumentTreeItem'; import { MongoClustersExtension } from './mongoClusters/MongoClustersExtension'; import { registerPostgresCommands } from './postgres/commands/registerPostgresCommands'; @@ -215,7 +214,7 @@ export async function activateInternal( 'cosmosDB.importDocument', async ( actionContext: IActionContext, - selectedNode: vscode.Uri | MongoCollectionTreeItem | DocDBCollectionTreeItem, + selectedNode: vscode.Uri | DocDBCollectionTreeItem, uris: vscode.Uri[], ) => { if (selectedNode instanceof vscode.Uri) { @@ -230,16 +229,13 @@ export async function activateInternal( 'cosmosDB.openDocument', async (actionContext: IActionContext, node?: DocDBDocumentTreeItem) => { if (!node) { - node = await ext.rgApi.pickAppResource( - actionContext, - { - filter: [cosmosMongoFilter, sqlFilter], - expectedChildContextValue: [ - MongoDocumentTreeItem.contextValue, - DocDBDocumentTreeItem.contextValue, - ], - }, - ); + node = await ext.rgApi.pickAppResource(actionContext, { + filter: [cosmosMongoFilter, sqlFilter], + expectedChildContextValue: [ + MongoDocumentTreeItem.contextValue, + DocDBDocumentTreeItem.contextValue, + ], + }); } // Clear un-uploaded local changes to the document before opening https://github.com/microsoft/vscode-cosmosdb/issues/1619 diff --git a/src/mongo/commands/createMongoCollection.ts b/src/mongo/commands/createMongoCollection.ts deleted file mode 100644 index b5493b457..000000000 --- a/src/mongo/commands/createMongoCollection.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as vscode from 'vscode'; -import { MongoDatabaseTreeItem } from '../tree/MongoDatabaseTreeItem'; -import { pickMongo } from './pickMongo'; - -export async function createMongoCollection(context: IActionContext, node?: MongoDatabaseTreeItem): Promise { - if (!node) { - node = await pickMongo(context, MongoDatabaseTreeItem.contextValue); - } - const collectionNode = await node.createChild(context); - await vscode.commands.executeCommand('cosmosDB.connectMongoDB', collectionNode.parent); -} diff --git a/src/mongo/commands/createMongoDatabase.ts b/src/mongo/commands/createMongoDatabase.ts deleted file mode 100644 index a527ecaea..000000000 --- a/src/mongo/commands/createMongoDatabase.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as vscode from 'vscode'; -import { type MongoAccountTreeItem } from '../tree/MongoAccountTreeItem'; -import { type MongoDatabaseTreeItem } from '../tree/MongoDatabaseTreeItem'; -import { pickMongo } from './pickMongo'; - -export async function createMongoDatabase(context: IActionContext, node?: MongoAccountTreeItem): Promise { - if (!node) { - node = await pickMongo(context); - } - const databaseNode = await node.createChild(context); - await databaseNode.createChild(context); - - await vscode.commands.executeCommand('cosmosDB.connectMongoDB', databaseNode); -} diff --git a/src/mongo/commands/deleteMongoCollection.ts b/src/mongo/commands/deleteMongoCollection.ts deleted file mode 100644 index 1cbe0a3e0..000000000 --- a/src/mongo/commands/deleteMongoCollection.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext, type ITreeItemPickerContext } from '@microsoft/vscode-azext-utils'; -import { MongoCollectionTreeItem } from '../tree/MongoCollectionTreeItem'; -import { pickMongo } from './pickMongo'; - -export async function deleteMongoCollection(context: IActionContext, node?: MongoCollectionTreeItem): Promise { - const suppressCreateContext: ITreeItemPickerContext = context; - suppressCreateContext.suppressCreatePick = true; - if (!node) { - node = await pickMongo(context, MongoCollectionTreeItem.contextValue); - } - await node.deleteTreeItem(context); -} diff --git a/src/mongo/commands/deleteMongoDatabase.ts b/src/mongo/commands/deleteMongoDatabase.ts deleted file mode 100644 index c9069e07b..000000000 --- a/src/mongo/commands/deleteMongoDatabase.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext, type ITreeItemPickerContext } from '@microsoft/vscode-azext-utils'; -import * as vscode from 'vscode'; -import { ext } from '../../extensionVariables'; -import { localize } from '../../utils/localize'; -import { setConnectedNode } from '../setConnectedNode'; -import { MongoDatabaseTreeItem } from '../tree/MongoDatabaseTreeItem'; -import { connectedMongoKey } from './connectMongoDatabase'; -import { pickMongo } from './pickMongo'; - -export async function deleteMongoDB(context: IActionContext, node?: MongoDatabaseTreeItem): Promise { - const suppressCreateContext: ITreeItemPickerContext = context; - suppressCreateContext.suppressCreatePick = true; - if (!node) { - node = await pickMongo(context, MongoDatabaseTreeItem.contextValue); - } - await node.deleteTreeItem(context); - if (ext.connectedMongoDB && ext.connectedMongoDB.fullId === node.fullId) { - setConnectedNode(undefined); - void ext.context.globalState.update(connectedMongoKey, undefined); - // Temporary workaround for https://github.com/microsoft/vscode-cosmosdb/issues/1754 - void ext.mongoLanguageClient.disconnect(); - } - const successMessage = localize('deleteMongoDatabaseMsg', 'Successfully deleted database "{0}"', node.databaseName); - void vscode.window.showInformationMessage(successMessage); - ext.outputChannel.info(successMessage); -} diff --git a/src/mongo/commands/deleteMongoDocument.ts b/src/mongo/commands/deleteMongoDocument.ts deleted file mode 100644 index 86e0122b9..000000000 --- a/src/mongo/commands/deleteMongoDocument.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext, type ITreeItemPickerContext } from '@microsoft/vscode-azext-utils'; -import { MongoDocumentTreeItem } from '../tree/MongoDocumentTreeItem'; -import { pickMongo } from './pickMongo'; - -export async function deleteMongoDocument(context: IActionContext, node?: MongoDocumentTreeItem): Promise { - const suppressCreateContext: ITreeItemPickerContext = context; - suppressCreateContext.suppressCreatePick = true; - if (!node) { - node = await pickMongo(context, MongoDocumentTreeItem.contextValue); - } - await node.deleteTreeItem(context); -} diff --git a/src/mongo/commands/openMongoCollection.ts b/src/mongo/commands/openMongoCollection.ts deleted file mode 100644 index b97f32c82..000000000 --- a/src/mongo/commands/openMongoCollection.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import { ext } from '../../extensionVariables'; -import { MongoCollectionTreeItem } from '../tree/MongoCollectionTreeItem'; -import { pickMongo } from './pickMongo'; - -export async function openMongoCollection(context: IActionContext, node?: MongoCollectionTreeItem): Promise { - if (!node) { - node = await pickMongo(context, MongoCollectionTreeItem.contextValue); - } - await ext.fileSystem.showTextDocument(node); -} diff --git a/src/mongo/registerMongoCommands.ts b/src/mongo/registerMongoCommands.ts index 390c00169..630548adb 100644 --- a/src/mongo/registerMongoCommands.ts +++ b/src/mongo/registerMongoCommands.ts @@ -14,16 +14,10 @@ import { import * as vscode from 'vscode'; import { ext } from '../extensionVariables'; import { connectMongoDatabase, loadPersistedMongoDB } from './commands/connectMongoDatabase'; -import { createMongoCollection } from './commands/createMongoCollection'; -import { createMongoDatabase } from './commands/createMongoDatabase'; import { createMongoSrapbook } from './commands/createMongoScrapbook'; -import { deleteMongoCollection } from './commands/deleteMongoCollection'; -import { deleteMongoDB } from './commands/deleteMongoDatabase'; -import { deleteMongoDocument } from './commands/deleteMongoDocument'; import { executeAllMongoCommand } from './commands/executeAllMongoCommand'; import { executeMongoCommand } from './commands/executeMongoCommand'; import { launchMongoShell } from './commands/launchMongoShell'; -import { openMongoCollection } from './commands/openMongoCollection'; import { MongoConnectError } from './connectToMongoClient'; import { MongoDBLanguageClient } from './languageClient'; import { getAllErrorsFromTextDocument } from './MongoScrapbook'; @@ -51,30 +45,9 @@ export function registerMongoCommands(): void { registerCommandWithTreeNodeUnwrapping('cosmosDB.executeMongoCommand', executeMongoCommand); registerCommandWithTreeNodeUnwrapping('cosmosDB.executeAllMongoCommands', executeAllMongoCommand); - // #region Account command - - registerCommandWithTreeNodeUnwrapping('cosmosDB.createMongoDatabase', createMongoDatabase); - - // #endregion - // #region Database command registerCommandWithTreeNodeUnwrapping('cosmosDB.connectMongoDB', connectMongoDatabase); - registerCommandWithTreeNodeUnwrapping('cosmosDB.createMongoCollection', createMongoCollection); - registerCommandWithTreeNodeUnwrapping('cosmosDB.deleteMongoDB', deleteMongoDB); - - // #endregion - - // #region Collection command - - registerCommandWithTreeNodeUnwrapping('cosmosDB.openCollection', openMongoCollection); - registerCommandWithTreeNodeUnwrapping('cosmosDB.deleteMongoCollection', deleteMongoCollection); - - // #endregion - - // #region Document command - - registerCommandWithTreeNodeUnwrapping('cosmosDB.deleteMongoDocument', deleteMongoDocument); // #endregion } diff --git a/src/mongo/tree/MongoAccountTreeItem.ts b/src/mongo/tree/MongoAccountTreeItem.ts index 3834b37d8..49bf08e81 100644 --- a/src/mongo/tree/MongoAccountTreeItem.ts +++ b/src/mongo/tree/MongoAccountTreeItem.ts @@ -11,7 +11,6 @@ import { parseError, type AzExtTreeItem, type IActionContext, - type ICreateChildImplContext, } from '@microsoft/vscode-azext-utils'; import { type MongoClient } from 'mongodb'; import type * as vscode from 'vscode'; @@ -132,17 +131,17 @@ export class MongoAccountTreeItem extends AzExtParentTreeItem { return result ?? []; } - public async createChildImpl(context: ICreateChildImplContext): Promise { - const databaseName = await context.ui.showInputBox({ - placeHolder: 'Database Name', - prompt: 'Enter the name of the database', - stepName: 'createMongoDatabase', - validateInput: validateDatabaseName, - }); - context.showCreatingTreeItem(databaseName); - - return new MongoDatabaseTreeItem(this, databaseName, this.connectionString); - } + // public async createChildImpl(context: ICreateChildImplContext): Promise { + // const databaseName = await context.ui.showInputBox({ + // placeHolder: 'Database Name', + // prompt: 'Enter the name of the database', + // stepName: 'createMongoDatabase', + // validateInput: validateDatabaseName, + // }); + // context.showCreatingTreeItem(databaseName); + + // return new MongoDatabaseTreeItem(this, databaseName, this.connectionString); + // } public isAncestorOfImpl(contextValue: string): boolean { switch (contextValue) { diff --git a/src/mongo/tree/MongoCollectionTreeItem.ts b/src/mongo/tree/MongoCollectionTreeItem.ts index c13b0e68e..9b825efa4 100644 --- a/src/mongo/tree/MongoCollectionTreeItem.ts +++ b/src/mongo/tree/MongoCollectionTreeItem.ts @@ -7,7 +7,6 @@ import { createGenericElement, - DialogResponses, type IActionContext, type TreeElementBase, type TreeElementWithId, @@ -256,14 +255,15 @@ export class MongoCollectionTreeItem implements TreeElementWithId { return { deferToShell: true, result: undefined }; } - public async deleteTreeItemImpl(context: IActionContext): Promise { - const message: string = `Are you sure you want to delete collection '${this.collection.collectionName}'?`; - await context.ui.showWarningMessage( - message, - { modal: true, stepName: 'deleteMongoCollection' }, - DialogResponses.deleteResponse, - ); - await this.drop(); + public async deleteTreeItemImpl(_context: IActionContext): Promise { + // TODO: this file is about to be deleted + // const message: string = `Are you sure you want to delete collection '${this.collection.collectionName}'?`; + // await context.ui.showWarningMessage( + // message, + // { modal: true, stepName: 'deleteMongoCollection' }, + // DialogResponses.deleteResponse, + // ); + // await this.drop(); } private async drop(): Promise { diff --git a/src/mongo/tree/MongoDocumentTreeItem.ts b/src/mongo/tree/MongoDocumentTreeItem.ts index 3d7918c1a..eea2b0df4 100644 --- a/src/mongo/tree/MongoDocumentTreeItem.ts +++ b/src/mongo/tree/MongoDocumentTreeItem.ts @@ -3,21 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { - AzExtTreeItem, - DialogResponses, - type IActionContext, - type TreeItemIconPath, -} from '@microsoft/vscode-azext-utils'; +import { AzExtTreeItem, type IActionContext, type TreeItemIconPath } from '@microsoft/vscode-azext-utils'; import { EJSON } from 'bson'; import { omit } from 'lodash'; -import { - type Collection, - type DeleteResult, - type Document as MongoDocument, - type ObjectId, - type UpdateResult, -} from 'mongodb'; +import { type Collection, type Document as MongoDocument, type ObjectId, type UpdateResult } from 'mongodb'; import * as vscode from 'vscode'; import { type IEditableTreeItem } from '../../DatabasesFileSystem'; import { ext } from '../../extensionVariables'; @@ -90,18 +79,18 @@ export class MongoDocumentTreeItem extends AzExtTreeItem implements IEditableTre ext.fileSystem.fireChangedEvent(this); } - public async deleteTreeItemImpl(context: IActionContext): Promise { - const message: string = `Are you sure you want to delete document '${this._label}'?`; - await context.ui.showWarningMessage( - message, - { modal: true, stepName: 'deleteMongoDocument' }, - DialogResponses.deleteResponse, - ); - const deleteResult: DeleteResult = await this.parent.collection.deleteOne({ _id: this.document._id }); - if (deleteResult.deletedCount !== 1) { - throw new Error(`Failed to delete document with _id '${this.document._id}'.`); - } - } + // public async deleteTreeItemImpl(context: IActionContext): Promise { + // const message: string = `Are you sure you want to delete document '${this._label}'?`; + // await context.ui.showWarningMessage( + // message, + // { modal: true, stepName: 'deleteMongoDocument' }, + // DialogResponses.deleteResponse, + // ); + // const deleteResult: DeleteResult = await this.parent.collection.deleteOne({ _id: this.document._id }); + // if (deleteResult.deletedCount !== 1) { + // throw new Error(`Failed to delete document with _id '${this.document._id}'.`); + // } + // } public async writeFileContent(_context: IActionContext, content: string): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call diff --git a/src/tree/CosmosDBBranchDataProvider.ts b/src/tree/CosmosDBBranchDataProvider.ts index 7a1685d65..2fbfd0269 100644 --- a/src/tree/CosmosDBBranchDataProvider.ts +++ b/src/tree/CosmosDBBranchDataProvider.ts @@ -3,7 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { + callWithTelemetryAndErrorHandling, + type IActionContext, + type TreeElementWithId, +} from '@microsoft/vscode-azext-utils'; import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; import { API, tryGetExperience } from '../AzureDBExperiences'; @@ -44,7 +48,7 @@ export class CosmosDBBranchDataProvider context.telemetry.properties.parentContext = elementTreeItem.contextValue ?? 'unknown'; return (await element.getChildren?.())?.map((child) => { - return ext.state.wrapItemInStateHandling(child, (child: CosmosDbTreeElement) => + return ext.state.wrapItemInStateHandling(child as TreeElementWithId, (child: CosmosDbTreeElement) => this.refresh(child), ) as CosmosDbTreeElement; }); From 2f2490c43ec4101e7be174526a9bf440024902a0 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 8 Jan 2025 14:25:59 +0100 Subject: [PATCH 18/42] MongoDB: V1->V2, removed unused commands, deleted and commented out obsolete code, wip --- package.json | 65 ---------------------------------------------------- 1 file changed, 65 deletions(-) diff --git a/package.json b/package.json index 416c5f9b9..96fb209db 100644 --- a/package.json +++ b/package.json @@ -355,16 +355,6 @@ "command": "cosmosDB.createGraphDatabase", "title": "Create Database..." }, - { - "category": "MongoDB", - "command": "cosmosDB.createMongoCollection", - "title": "Create Collection..." - }, - { - "category": "MongoDB", - "command": "cosmosDB.createMongoDatabase", - "title": "Create Database..." - }, { "category": "Cosmos DB", "command": "cosmosDB.deleteAccount", @@ -405,21 +395,6 @@ "command": "cosmosDB.deleteGraphDatabase", "title": "Delete Database..." }, - { - "category": "MongoDB", - "command": "cosmosDB.deleteMongoCollection", - "title": "Delete Collection..." - }, - { - "category": "MongoDB", - "command": "cosmosDB.deleteMongoDB", - "title": "Delete Database..." - }, - { - "category": "MongoDB", - "command": "cosmosDB.deleteMongoDocument", - "title": "Delete Document..." - }, { "category": "MongoDB", "command": "cosmosDB.executeAllMongoCommands", @@ -463,11 +438,6 @@ "title": "New Mongo Scrapbook", "icon": "$(new-file)" }, - { - "category": "MongoDB", - "command": "cosmosDB.openCollection", - "title": "Open Collection" - }, { "category": "Cosmos DB", "command": "cosmosDB.openDocument", @@ -746,21 +716,6 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /postgresServer(?![a-z])/i", "group": "1@2" }, - { - "command": "cosmosDB.createMongoDatabase", - "when": "view == azureWorkspace && viewItem == cosmosDBMongoServerAttached", - "group": "1@1" - }, - { - "command": "cosmosDB.createMongoDatabase", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBMongoServer(?![a-z])/i", - "group": "1@1" - }, - { - "command": "cosmosDB.createMongoCollection", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == mongoDb", - "group": "1@1" - }, { "command": "cosmosDB.createDocDBDocument", "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentsGroup", @@ -871,21 +826,6 @@ "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == mongoDb", "group": "2@1" }, - { - "command": "cosmosDB.deleteMongoDB", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == mongoDb", - "group": "1@2" - }, - { - "command": "cosmosDB.deleteMongoCollection", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == MongoCollection", - "group": "1@4" - }, - { - "command": "cosmosDB.deleteMongoDocument", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == MongoDocument", - "group": "1@2" - }, { "command": "cosmosDB.deleteDocDBCollection", "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", @@ -966,11 +906,6 @@ "when": "view == azureWorkspace && viewItem == cosmosDBAttachedAccountsWithEmulator", "group": "1@2" }, - { - "command": "cosmosDB.openCollection", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == MongoCollection", - "group": "1@2" - }, { "command": "cosmosDB.copyConnectionString", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBMongoServer(?![a-z])/i", From 5ad2fd0cf0a40591ec7adb970a32d9bc526298b1 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 8 Jan 2025 14:36:35 +0100 Subject: [PATCH 19/42] resolved build errors, wip --- src/mongo/tree/MongoCollectionTreeItem.ts | 333 +++++++++++----------- src/mongo/tree/MongoDatabaseTreeItem.ts | 124 ++++---- src/mongo/tree/MongoDocumentTreeItem.ts | 39 ++- 3 files changed, 251 insertions(+), 245 deletions(-) diff --git a/src/mongo/tree/MongoCollectionTreeItem.ts b/src/mongo/tree/MongoCollectionTreeItem.ts index 9b825efa4..30cb61dd2 100644 --- a/src/mongo/tree/MongoCollectionTreeItem.ts +++ b/src/mongo/tree/MongoCollectionTreeItem.ts @@ -6,28 +6,37 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - createGenericElement, + AzExtParentTreeItem, + DialogResponses, + type AzExtTreeItem, type IActionContext, - type TreeElementBase, - type TreeElementWithId, + type ICreateChildImplContext, + type TreeItemIconPath, } from '@microsoft/vscode-azext-utils'; import assert from 'assert'; import { EJSON } from 'bson'; +import { omit } from 'lodash'; import { + type AnyBulkWriteOperation, type BulkWriteOptions, + type BulkWriteResult, type Collection, type CountOptions, type DeleteResult, type Filter, + type FindCursor, type InsertManyResult, type InsertOneResult, type Document as MongoDocument, } from 'mongodb'; import * as vscode from 'vscode'; -import { ThemeIcon, type TreeItem } from 'vscode'; -import { type MongoAccountModel } from '../../tree/mongo/MongoAccountModel'; +import { type IEditableTreeItem } from '../../DatabasesFileSystem'; +import { ext } from '../../extensionVariables'; +import { nonNullValue } from '../../utils/nonNull'; +import { getDocumentTreeItemLabel } from '../../utils/vscodeUtils'; +import { getBatchSizeSetting } from '../../utils/workspacUtils'; import { type MongoCommand } from '../MongoCommand'; -import { type IDatabaseInfo } from './MongoAccountTreeItem'; +import { MongoDocumentTreeItem, type IMongoDocument } from './MongoDocumentTreeItem'; // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types type MongoFunction = (...args: (Object | Object[] | undefined)[]) => Thenable; @@ -41,172 +50,161 @@ class FunctionDescriptor { ) {} } -// export class MongoCollectionTreeItem extends AzExtParentTreeItem implements IEditableTreeItem { -export class MongoCollectionTreeItem implements TreeElementWithId { - // public static contextValue: string = 'MongoCollection'; - // public readonly contextValue: string = MongoCollectionTreeItem.contextValue; - // public readonly childTypeLabel: string = 'Document'; - // public readonly collection: Collection; - // public declare parent: AzExtParentTreeItem; +export class MongoCollectionTreeItem extends AzExtParentTreeItem implements IEditableTreeItem { + public static contextValue: string = 'MongoCollection'; + public readonly contextValue: string = MongoCollectionTreeItem.contextValue; + public readonly childTypeLabel: string = 'Document'; + public readonly collection: Collection; + public declare parent: AzExtParentTreeItem; // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types public findArgs?: Object[]; public readonly cTime: number = Date.now(); public mTime: number = Date.now(); - id: string; + private readonly _query: Filter | undefined; + private readonly _projection: object | undefined; + private _cursor: FindCursor | undefined; + private _hasMoreChildren: boolean = true; + private _batchSize: number = getBatchSizeSetting(); // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types - constructor( - readonly account: MongoAccountModel, - readonly databaseInfo: IDatabaseInfo, - readonly collection: Collection, - ) { - this.id = `${account.id}/${databaseInfo.name}/${collection.collectionName}`; + constructor(parent: AzExtParentTreeItem, collection: Collection, findArgs?: Object[]) { + super(parent); + this.collection = collection; + this.findArgs = findArgs; + if (findArgs && findArgs.length) { + this._query = findArgs[0]; + this._projection = findArgs.length > 1 ? findArgs[1] : undefined; + } + ext.fileSystem.fireChangedEvent(this); } - getChildren?(): TreeElementBase[] { - return [ - createGenericElement({ - contextValue: 'mongo.item.documents', - id: `${this.id}/documents`, - label: 'Documents', - // commandId: 'command.internal.mongoClusters.containerView.open', - commandArgs: [ - { - id: this.id, - // viewTitle: `${this.collectionInfo.name}`, - // // viewTitle: `${this.mongoCluster.name}/${this.databaseInfo.name}/${this.collectionInfo.name}`, // using '/' as a separator to use VSCode's "title compression"(?) feature - - // liveConnectionId: this.mongoCluster.id, - // databaseName: this.databaseInfo.name, - // collectionName: this.collectionInfo.name, - // collectionTreeItem: this, - }, - ], - }), - ]; + public async writeFileContent(context: IActionContext, content: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + const documents: IMongoDocument[] = EJSON.parse(content); + const operations: AnyBulkWriteOperation[] = documents.map((document) => { + return { + replaceOne: { + filter: { _id: document._id }, + replacement: omit(document, '_id'), + upsert: false, + }, + }; + }); + + const result: BulkWriteResult = await this.collection.bulkWrite(operations); + ext.outputChannel.appendLog( + `Successfully updated ${result.modifiedCount} document(s), inserted ${result.insertedCount} document(s)`, + ); + + // The current tree item may have been a temporary one used to execute a scrapbook command. + // We want to refresh children for this one _and_ the actual one in the tree (if it's different) + const nodeInTree: MongoCollectionTreeItem | undefined = await ext.rgApi.appResourceTree.findTreeItem( + this.fullId, + context, + ); + const nodesToRefresh: MongoCollectionTreeItem[] = [this]; + if (nodeInTree && this !== nodeInTree) { + nodesToRefresh.push(nodeInTree); + } + + await Promise.all(nodesToRefresh.map((n) => n.refreshChildren(context, documents))); + + if (nodeInTree && this !== nodeInTree) { + // Don't need to fire a changed event on the item being saved at the moment. Just the node in the tree if it's different + ext.fileSystem.fireChangedEvent(nodeInTree); + } } - getTreeItem(): TreeItem { - return { - id: this.id, - contextValue: 'mongo.item.collection', - label: this.collection.collectionName, - iconPath: new ThemeIcon('folder-opened'), - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - }; + public async getFileContent(context: IActionContext): Promise { + const children = await this.getCachedChildren(context); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + return EJSON.stringify( + children.map((c) => c.document), + undefined, + 2, + ); + } + + public get id(): string { + return this.collection.collectionName; + } + + public get label(): string { + return this.collection.collectionName; + } + + public get iconPath(): TreeItemIconPath { + return new vscode.ThemeIcon('files'); + } + + public get filePath(): string { + return this.label + '-cosmos-collection.json'; + } + + public async refreshImpl(): Promise { + this._batchSize = getBatchSizeSetting(); + ext.fileSystem.fireChangedEvent(this); } - // public async writeFileContent(context: IActionContext, content: string): Promise { - // // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - // const documents: IMongoDocument[] = EJSON.parse(content); - // const operations: AnyBulkWriteOperation[] = documents.map((document) => { - // return { - // replaceOne: { - // filter: { _id: document._id }, - // replacement: omit(document, '_id'), - // upsert: false, - // }, - // }; - // }); - - // const result: BulkWriteResult = await this.collection.bulkWrite(operations); - // ext.outputChannel.appendLog( - // `Successfully updated ${result.modifiedCount} document(s), inserted ${result.insertedCount} document(s)`, - // ); - - // // The current tree item may have been a temporary one used to execute a scrapbook command. - // // We want to refresh children for this one _and_ the actual one in the tree (if it's different) - // const nodeInTree: MongoCollectionTreeItem | undefined = await ext.rgApi.appResourceTree.findTreeItem( - // this.fullId, - // context, - // ); - // const nodesToRefresh: MongoCollectionTreeItem[] = [this]; - // if (nodeInTree && this !== nodeInTree) { - // nodesToRefresh.push(nodeInTree); - // } - - // await Promise.all(nodesToRefresh.map((n) => n.refreshChildren(context, documents))); - - // if (nodeInTree && this !== nodeInTree) { - // // Don't need to fire a changed event on the item being saved at the moment. Just the node in the tree if it's different - // ext.fileSystem.fireChangedEvent(nodeInTree); - // } - // } - - // public async getFileContent(context: IActionContext): Promise { - // const children = await this.getCachedChildren(context); - // // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - // return EJSON.stringify( - // children.map((c) => c.document), - // undefined, - // 2, - // ); - // } - - // public get filePath(): string { - // return this.label + '-cosmos-collection.json'; - // } - - // public async refreshImpl(): Promise { - // this._batchSize = getBatchSizeSetting(); - // ext.fileSystem.fireChangedEvent(this); - // } - - // public async refreshChildren(context: IActionContext, docs: IMongoDocument[]): Promise { - // const documentNodes = await this.getCachedChildren(context); - // for (const doc of docs) { - // const documentNode = documentNodes.find((node) => node.document._id.toString() === doc._id.toString()); - // if (documentNode) { - // documentNode.document = doc; - // await documentNode.refresh(context); - // } - // } - // } - - // public async loadMoreChildrenImpl(clearCache: boolean): Promise { - // if (clearCache || this._cursor === undefined) { - // if (this._query) { - // this._cursor = this.collection.find(this._query).batchSize(this._batchSize); - // } else { - // this._cursor = this.collection.find().batchSize(this._batchSize); - // } - // if (this._projection) { - // this._cursor = this._cursor.project(this._projection); - // } - // } - - // const documents: IMongoDocument[] = []; - // let count: number = 0; - // while (count < this._batchSize) { - // this._hasMoreChildren = await this._cursor.hasNext(); - // if (this._hasMoreChildren) { - // documents.push(await this._cursor.next()); - // count += 1; - // } else { - // break; - // } - // } - // this._batchSize *= 2; - - // return this.createTreeItemsWithErrorHandling( - // documents, - // 'invalidMongoDocument', - // (doc) => new MongoDocumentTreeItem(this, doc), - // getDocumentTreeItemLabel, - // ); - // } - - // public async createChildImpl(context: ICreateChildImplContext): Promise { - // context.showCreatingTreeItem(''); - // // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - // const result: InsertOneResult = await this.collection.insertOne({}); - // const newDocument: IMongoDocument = nonNullValue( - // await this.collection.findOne({ _id: result.insertedId }), - // 'newDocument', - // ); - // return new MongoDocumentTreeItem(this, newDocument); - // } + public async refreshChildren(context: IActionContext, docs: IMongoDocument[]): Promise { + const documentNodes = await this.getCachedChildren(context); + for (const doc of docs) { + const documentNode = documentNodes.find((node) => node.document._id.toString() === doc._id.toString()); + if (documentNode) { + documentNode.document = doc; + await documentNode.refresh(context); + } + } + } + + public hasMoreChildrenImpl(): boolean { + return this._hasMoreChildren; + } + + public async loadMoreChildrenImpl(clearCache: boolean): Promise { + if (clearCache || this._cursor === undefined) { + if (this._query) { + this._cursor = this.collection.find(this._query).batchSize(this._batchSize); + } else { + this._cursor = this.collection.find().batchSize(this._batchSize); + } + if (this._projection) { + this._cursor = this._cursor.project(this._projection); + } + } + + const documents: IMongoDocument[] = []; + let count: number = 0; + while (count < this._batchSize) { + this._hasMoreChildren = await this._cursor.hasNext(); + if (this._hasMoreChildren) { + documents.push(await this._cursor.next()); + count += 1; + } else { + break; + } + } + this._batchSize *= 2; + + return this.createTreeItemsWithErrorHandling( + documents, + 'invalidMongoDocument', + (doc) => new MongoDocumentTreeItem(this, doc), + getDocumentTreeItemLabel, + ); + } + + public async createChildImpl(context: ICreateChildImplContext): Promise { + context.showCreatingTreeItem(''); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const result: InsertOneResult = await this.collection.insertOne({}); + const newDocument: IMongoDocument = nonNullValue( + await this.collection.findOne({ _id: result.insertedId }), + 'newDocument', + ); + return new MongoDocumentTreeItem(this, newDocument); + } public async tryExecuteCommandDirectly( command: Partial, @@ -255,15 +253,14 @@ export class MongoCollectionTreeItem implements TreeElementWithId { return { deferToShell: true, result: undefined }; } - public async deleteTreeItemImpl(_context: IActionContext): Promise { - // TODO: this file is about to be deleted - // const message: string = `Are you sure you want to delete collection '${this.collection.collectionName}'?`; - // await context.ui.showWarningMessage( - // message, - // { modal: true, stepName: 'deleteMongoCollection' }, - // DialogResponses.deleteResponse, - // ); - // await this.drop(); + public async deleteTreeItemImpl(context: IActionContext): Promise { + const message: string = `Are you sure you want to delete collection '${this.label}'?`; + await context.ui.showWarningMessage( + message, + { modal: true, stepName: 'deleteMongoCollection' }, + DialogResponses.deleteResponse, + ); + await this.drop(); } private async drop(): Promise { diff --git a/src/mongo/tree/MongoDatabaseTreeItem.ts b/src/mongo/tree/MongoDatabaseTreeItem.ts index 8ebaf42b6..a36df8026 100644 --- a/src/mongo/tree/MongoDatabaseTreeItem.ts +++ b/src/mongo/tree/MongoDatabaseTreeItem.ts @@ -5,19 +5,19 @@ import { appendExtensionUserAgent, + AzExtParentTreeItem, + DialogResponses, UserCancelledError, type IActionContext, - type TreeElementBase, + type ICreateChildImplContext, + type TreeItemIconPath, } from '@microsoft/vscode-azext-utils'; import * as fse from 'fs-extra'; import { type Collection, type CreateCollectionOptions, type Db } from 'mongodb'; import * as path from 'path'; import * as process from 'process'; import * as vscode from 'vscode'; -import { type TreeItem } from 'vscode'; import { ext } from '../../extensionVariables'; -import { type IDatabaseInfo } from '../../tree/mongo/IDatabaseInfo'; -import { type MongoAccountModel } from '../../tree/mongo/MongoAccountModel'; import * as cpUtils from '../../utils/cp'; import { nonNullProp, nonNullValue } from '../../utils/nonNull'; import { connectToMongoClient } from '../connectToMongoClient'; @@ -31,81 +31,79 @@ import { MongoCollectionTreeItem } from './MongoCollectionTreeItem'; const mongoExecutableFileName = process.platform === 'win32' ? 'mongo.exe' : 'mongo'; const executingInShellMsg = 'Executing command in Mongo shell'; -export class MongoDatabaseTreeItem implements TreeElementBase { - // public static contextValue: string = 'mongoDb'; - // public readonly contextValue: string = MongoDatabaseTreeItem.contextValue; - // public readonly childTypeLabel: string = 'Collection'; +export class MongoDatabaseTreeItem extends AzExtParentTreeItem { + public static contextValue: string = 'mongoDb'; + public readonly contextValue: string = MongoDatabaseTreeItem.contextValue; + public readonly childTypeLabel: string = 'Collection'; public readonly connectionString: string; + public readonly databaseName: string; public declare readonly parent: MongoAccountTreeItem; private _previousShellPathSetting: string | undefined; private _cachedShellPathOrCmd: string | undefined; - id: string; + constructor(parent: MongoAccountTreeItem, databaseName: string, connectionString: string) { + super(parent); + this.databaseName = databaseName; + this.connectionString = addDatabaseToAccountConnectionString(connectionString, this.databaseName); + } + + public get root(): IMongoTreeRoot { + return this.parent.root; + } - constructor( - readonly account: MongoAccountModel, - readonly databaseInfo: IDatabaseInfo, - ) { - this.id = `${account.id}/${databaseInfo.name}`; + public get label(): string { + return this.databaseName; + } + + public get description(): string { + return ext.connectedMongoDB && ext.connectedMongoDB.fullId === this.fullId ? 'Connected' : ''; + } + + public get id(): string { + return this.databaseName; + } - this.connectionString = addDatabaseToAccountConnectionString(account.connectionString, databaseInfo.name); + public get iconPath(): TreeItemIconPath { + return new vscode.ThemeIcon('database'); } - async getChildren(): Promise { + public hasMoreChildrenImpl(): boolean { + return false; + } + + public async loadMoreChildrenImpl(_clearCache: boolean): Promise { const db: Db = await this.connectToDb(); const collections: Collection[] = await db.collections(); - return collections.map( - (collection) => new MongoCollectionTreeItem(this.account, this.databaseInfo, collection), - ); - } - getTreeItem(): TreeItem { - return { - id: this.id, - contextValue: 'mongo.item.database', - label: this.databaseInfo.name, - iconPath: new vscode.ThemeIcon('database'), - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - description: ext.connectedMongoDB && ext.connectedMongoDB.id === this.id ? 'Connected' : '', - }; + return collections.map((collection) => new MongoCollectionTreeItem(this, collection)); } - public get root(): IMongoTreeRoot { - return this.parent.root; + public async createChildImpl(context: ICreateChildImplContext): Promise { + const collectionName = await context.ui.showInputBox({ + placeHolder: 'Collection Name', + prompt: 'Enter the name of the collection', + stepName: 'createMongoCollection', + validateInput: validateMongoCollectionName, + }); + + context.showCreatingTreeItem(collectionName); + return await this.createCollection(collectionName); } - // public async loadMoreChildrenImpl(_clearCache: boolean): Promise { - // const db: Db = await this.connectToDb(); - // const collections: Collection[] = await db.collections(); - // return collections.map((collection) => new MongoCollectionTreeItem(this, collection)); - // } - - // public async createChildImpl(context: ICreateChildImplContext): Promise { - // const collectionName = await context.ui.showInputBox({ - // placeHolder: 'Collection Name', - // prompt: 'Enter the name of the collection', - // stepName: 'createMongoCollection', - // validateInput: validateMongoCollectionName, - // }); - - // context.showCreatingTreeItem(collectionName); - // return await this.createCollection(collectionName); - // } - - // public async deleteTreeItemImpl(context: IActionContext): Promise { - // const message: string = `Are you sure you want to delete database '${this.databaseInfo.name}'?`; - // await context.ui.showWarningMessage( - // message, - // { modal: true, stepName: 'deleteMongoDatabase' }, - // DialogResponses.deleteResponse, - // ); - // const db = await this.connectToDb(); - // await db.dropDatabase(); - // } + public async deleteTreeItemImpl(context: IActionContext): Promise { + const message: string = `Are you sure you want to delete database '${this.label}'?`; + await context.ui.showWarningMessage( + message, + { modal: true, stepName: 'deleteMongoDatabase' }, + DialogResponses.deleteResponse, + ); + const db = await this.connectToDb(); + await db.dropDatabase(); + } public async connectToDb(): Promise { const accountConnection = await connectToMongoClient(this.connectionString, appendExtensionUserAgent()); - return accountConnection.db(this.databaseInfo.name); + return accountConnection.db(this.databaseName); } public async executeCommand(command: MongoCommand, context: IActionContext): Promise { @@ -113,7 +111,7 @@ export class MongoDatabaseTreeItem implements TreeElementBase { const db = await this.connectToDb(); const collection = db.collection(command.collection); if (collection) { - const collectionTreeItem = new MongoCollectionTreeItem(this.account, this.databaseInfo, collection); + const collectionTreeItem = new MongoCollectionTreeItem(this, collection, command.arguments); const result = await collectionTreeItem.tryExecuteCommandDirectly(command); if (!result.deferToShell) { return result.result; @@ -147,7 +145,7 @@ export class MongoDatabaseTreeItem implements TreeElementBase { const result = await newCollection.insertOne({}); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment await newCollection.deleteOne({ _id: result.insertedId }); - return new MongoCollectionTreeItem(this.account, this.databaseInfo, newCollection); + return new MongoCollectionTreeItem(this, newCollection); } private async executeCommandInShell(command: MongoCommand, context: IActionContext): Promise { @@ -163,7 +161,7 @@ export class MongoDatabaseTreeItem implements TreeElementBase { // requests. const shell = await this.createShell(context); try { - await shell.useDatabase(this.databaseInfo.name); + await shell.useDatabase(this.databaseName); return await shell.executeScript(command.text); } finally { shell.dispose(); diff --git a/src/mongo/tree/MongoDocumentTreeItem.ts b/src/mongo/tree/MongoDocumentTreeItem.ts index eea2b0df4..3d7918c1a 100644 --- a/src/mongo/tree/MongoDocumentTreeItem.ts +++ b/src/mongo/tree/MongoDocumentTreeItem.ts @@ -3,10 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzExtTreeItem, type IActionContext, type TreeItemIconPath } from '@microsoft/vscode-azext-utils'; +import { + AzExtTreeItem, + DialogResponses, + type IActionContext, + type TreeItemIconPath, +} from '@microsoft/vscode-azext-utils'; import { EJSON } from 'bson'; import { omit } from 'lodash'; -import { type Collection, type Document as MongoDocument, type ObjectId, type UpdateResult } from 'mongodb'; +import { + type Collection, + type DeleteResult, + type Document as MongoDocument, + type ObjectId, + type UpdateResult, +} from 'mongodb'; import * as vscode from 'vscode'; import { type IEditableTreeItem } from '../../DatabasesFileSystem'; import { ext } from '../../extensionVariables'; @@ -79,18 +90,18 @@ export class MongoDocumentTreeItem extends AzExtTreeItem implements IEditableTre ext.fileSystem.fireChangedEvent(this); } - // public async deleteTreeItemImpl(context: IActionContext): Promise { - // const message: string = `Are you sure you want to delete document '${this._label}'?`; - // await context.ui.showWarningMessage( - // message, - // { modal: true, stepName: 'deleteMongoDocument' }, - // DialogResponses.deleteResponse, - // ); - // const deleteResult: DeleteResult = await this.parent.collection.deleteOne({ _id: this.document._id }); - // if (deleteResult.deletedCount !== 1) { - // throw new Error(`Failed to delete document with _id '${this.document._id}'.`); - // } - // } + public async deleteTreeItemImpl(context: IActionContext): Promise { + const message: string = `Are you sure you want to delete document '${this._label}'?`; + await context.ui.showWarningMessage( + message, + { modal: true, stepName: 'deleteMongoDocument' }, + DialogResponses.deleteResponse, + ); + const deleteResult: DeleteResult = await this.parent.collection.deleteOne({ _id: this.document._id }); + if (deleteResult.deletedCount !== 1) { + throw new Error(`Failed to delete document with _id '${this.document._id}'.`); + } + } public async writeFileContent(_context: IActionContext, content: string): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call From 674ad1001e27db4affba93e9fde7c7beecd34c52 Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Thu, 9 Jan 2025 10:24:18 +0100 Subject: [PATCH 20/42] feat: Migrating TreeView to V2 --- src/constants.ts | 2 + src/docdb/commands/loadMore.ts | 18 ++ src/docdb/registerDocDBCommands.ts | 2 + src/docdb/utils/rbacUtils.ts | 35 +++ src/extension.ts | 10 + src/extensionVariables.ts | 2 +- src/mongoClusters/MongoClustersExtension.ts | 10 +- .../commands/addWorkspaceConnection.ts | 4 +- .../commands/removeWorkspaceConnection.ts | 4 +- .../workspace/MongoDBAccountsWorkspaceItem.ts | 4 +- src/tree/AttachedAccountsTreeItem.ts | 2 +- src/tree/CosmosAccountResourceItemBase.ts | 7 +- src/tree/CosmosDBBranchDataProvider.ts | 31 ++- ...bTreeElement.ts => CosmosDBTreeElement.ts} | 2 +- .../CosmosDBWorkspaceBranchDataProvider.ts | 76 ++++++ src/tree/DocumentDBAccountResourceItem.ts | 114 --------- .../CosmosDBAttachedAccountModel.ts} | 9 +- .../CosmosDBAttachedAccountsResourceItem.ts | 216 ++++++++++++++++++ .../AccountInfo.ts} | 10 +- .../DocumentDBAccountAttachedResourceItem.ts | 129 +++++++++++ .../docdb/DocumentDBAccountResourceItem.ts | 178 +++++++++++++++ .../docdb/DocumentDBContainerResourceItem.ts | 53 +++++ .../docdb/DocumentDBDatabaseResourceItem.ts | 58 +++++ src/tree/docdb/DocumentDBItemResourceItem.ts | 86 +++++++ src/tree/docdb/DocumentDBItemsResourceItem.ts | 113 +++++++++ .../DocumentDBStoredProcedureResourceItem.ts | 37 +++ .../DocumentDBStoredProceduresResourceItem.ts | 65 ++++++ .../docdb/DocumentDBTriggerResourceItem.ts | 37 +++ .../docdb/DocumentDBTriggersResourceItem.ts | 62 +++++ .../models/DocumentDBAccountModel.ts} | 4 +- .../docdb/models/DocumentDBContainerModel.ts | 13 ++ .../docdb/models/DocumentDBDatabaseModel.ts | 12 + src/tree/docdb/models/DocumentDBItemModel.ts | 14 ++ src/tree/docdb/models/DocumentDBItemsModel.ts | 13 ++ .../models/DocumentDBStoredProcedureModel.ts | 19 ++ .../models/DocumentDBStoredProceduresModel.ts | 13 ++ .../docdb/models/DocumentDBTriggerModel.ts | 19 ++ .../docdb/models/DocumentDBTriggersModel.ts | 13 ++ .../graph/GraphAccountAttachedResourceItem.ts | 35 +++ src/tree/graph/GraphAccountResourceItem.ts | 44 ++-- src/tree/graph/GraphContainerResourceItem.ts | 29 +++ src/tree/graph/GraphDatabaseResourceItem.ts | 24 ++ src/tree/graph/GraphItemResourceItem.ts | 14 ++ src/tree/graph/GraphItemsResourceItem.ts | 23 ++ .../graph/GraphStoredProcedureResourceItem.ts | 14 ++ .../GraphStoredProceduresResourceItem.ts | 27 +++ src/tree/mongo/DatabaseItem.ts | 8 +- src/tree/mongo/MongoAccountResourceItem.ts | 4 +- .../nosql/NoSqlAccountAttachedResourceItem.ts | 35 +++ src/tree/nosql/NoSqlAccountResourceItem.ts | 31 ++- src/tree/nosql/NoSqlContainerResourceItem.ts | 30 +++ src/tree/nosql/NoSqlDatabaseResourceItem.ts | 24 ++ src/tree/nosql/NoSqlItemResourceItem.ts | 14 ++ src/tree/nosql/NoSqlItemsResourceItem.ts | 23 ++ .../nosql/NoSqlStoredProcedureResourceItem.ts | 14 ++ .../NoSqlStoredProceduresResourceItem.ts | 27 +++ src/tree/nosql/NoSqlTriggerResourceItem.ts | 14 ++ src/tree/nosql/NoSqlTriggersResourceItem.ts | 23 ++ .../table/TableAccountAttachedResourceItem.ts | 41 ++++ src/tree/table/TableAccountResourceItem.ts | 43 +++- ....ts => SharedWorkspaceResourceProvider.ts} | 6 + ...ceStorage.ts => SharedWorkspaceStorage.ts} | 2 +- src/utils/document.ts | 2 +- 63 files changed, 1852 insertions(+), 195 deletions(-) create mode 100644 src/docdb/commands/loadMore.ts rename src/tree/{CosmosDbTreeElement.ts => CosmosDBTreeElement.ts} (92%) create mode 100644 src/tree/CosmosDBWorkspaceBranchDataProvider.ts delete mode 100644 src/tree/DocumentDBAccountResourceItem.ts rename src/tree/{graph/GraphAccountModel.ts => attached/CosmosDBAttachedAccountModel.ts} (72%) create mode 100644 src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts rename src/tree/{nosql/NoSqlAccountModel.ts => docdb/AccountInfo.ts} (61%) create mode 100644 src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts create mode 100644 src/tree/docdb/DocumentDBAccountResourceItem.ts create mode 100644 src/tree/docdb/DocumentDBContainerResourceItem.ts create mode 100644 src/tree/docdb/DocumentDBDatabaseResourceItem.ts create mode 100644 src/tree/docdb/DocumentDBItemResourceItem.ts create mode 100644 src/tree/docdb/DocumentDBItemsResourceItem.ts create mode 100644 src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts create mode 100644 src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts create mode 100644 src/tree/docdb/DocumentDBTriggerResourceItem.ts create mode 100644 src/tree/docdb/DocumentDBTriggersResourceItem.ts rename src/tree/{table/TableAccountModel.ts => docdb/models/DocumentDBAccountModel.ts} (73%) create mode 100644 src/tree/docdb/models/DocumentDBContainerModel.ts create mode 100644 src/tree/docdb/models/DocumentDBDatabaseModel.ts create mode 100644 src/tree/docdb/models/DocumentDBItemModel.ts create mode 100644 src/tree/docdb/models/DocumentDBItemsModel.ts create mode 100644 src/tree/docdb/models/DocumentDBStoredProcedureModel.ts create mode 100644 src/tree/docdb/models/DocumentDBStoredProceduresModel.ts create mode 100644 src/tree/docdb/models/DocumentDBTriggerModel.ts create mode 100644 src/tree/docdb/models/DocumentDBTriggersModel.ts create mode 100644 src/tree/graph/GraphAccountAttachedResourceItem.ts create mode 100644 src/tree/graph/GraphContainerResourceItem.ts create mode 100644 src/tree/graph/GraphDatabaseResourceItem.ts create mode 100644 src/tree/graph/GraphItemResourceItem.ts create mode 100644 src/tree/graph/GraphItemsResourceItem.ts create mode 100644 src/tree/graph/GraphStoredProcedureResourceItem.ts create mode 100644 src/tree/graph/GraphStoredProceduresResourceItem.ts create mode 100644 src/tree/nosql/NoSqlAccountAttachedResourceItem.ts create mode 100644 src/tree/nosql/NoSqlContainerResourceItem.ts create mode 100644 src/tree/nosql/NoSqlDatabaseResourceItem.ts create mode 100644 src/tree/nosql/NoSqlItemResourceItem.ts create mode 100644 src/tree/nosql/NoSqlItemsResourceItem.ts create mode 100644 src/tree/nosql/NoSqlStoredProcedureResourceItem.ts create mode 100644 src/tree/nosql/NoSqlStoredProceduresResourceItem.ts create mode 100644 src/tree/nosql/NoSqlTriggerResourceItem.ts create mode 100644 src/tree/nosql/NoSqlTriggersResourceItem.ts create mode 100644 src/tree/table/TableAccountAttachedResourceItem.ts rename src/tree/workspace/{sharedWorkspaceResourceProvider.ts => SharedWorkspaceResourceProvider.ts} (90%) rename src/tree/workspace/{sharedWorkspaceStorage.ts => SharedWorkspaceStorage.ts} (99%) diff --git a/src/constants.ts b/src/constants.ts index 97c59147a..0ed837df2 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -153,3 +153,5 @@ export const postgresFlexibleFilter = { export const postgresSingleFilter = { type: 'Microsoft.DBForPostgreSQL/servers', }; + +export const DocumentDBHiddenFields: string[] = ['_rid', '_self', '_etag', '_attachments', '_ts']; diff --git a/src/docdb/commands/loadMore.ts b/src/docdb/commands/loadMore.ts new file mode 100644 index 000000000..1e6ec47fc --- /dev/null +++ b/src/docdb/commands/loadMore.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import { ext } from '../../extensionVariables'; + +export async function loadMore( + context: IActionContext, + nodeId: string, + loadMoreFn: (context: IActionContext) => Promise | undefined, +): Promise { + if (loadMoreFn) { + await loadMoreFn(context); + ext.state.notifyChildrenChanged(nodeId); + } +} diff --git a/src/docdb/registerDocDBCommands.ts b/src/docdb/registerDocDBCommands.ts index 6263bd47a..41318481f 100644 --- a/src/docdb/registerDocDBCommands.ts +++ b/src/docdb/registerDocDBCommands.ts @@ -21,6 +21,7 @@ import { deleteDocDBTrigger } from './commands/deleteDocDBTrigger'; import { executeDocDBStoredProcedure } from './commands/executeDocDBStoredProcedure'; import { executeNoSqlQuery } from './commands/executeNoSqlQuery'; import { getNoSqlQueryPlan } from './commands/getNoSqlQueryPlan'; +import { loadMore } from './commands/loadMore'; import { openNoSqlQueryEditor } from './commands/openNoSqlQueryEditor'; import { openStoredProcedure } from './commands/openStoredProcedure'; import { openTrigger } from './commands/openTrigger'; @@ -34,6 +35,7 @@ export function registerDocDBCommands(): void { ext.noSqlCodeLensProvider = new NoSqlCodeLensProvider(); ext.context.subscriptions.push(languages.registerCodeLensProvider(nosqlLanguageId, ext.noSqlCodeLensProvider)); + registerCommand('cosmosDB.loadMore', loadMore); registerCommand('cosmosDB.connectNoSqlContainer', connectNoSqlContainer); registerCommand('cosmosDB.executeNoSqlQuery', executeNoSqlQuery); registerCommand('cosmosDB.getNoSqlQueryPlan', getNoSqlQueryPlan); diff --git a/src/docdb/utils/rbacUtils.ts b/src/docdb/utils/rbacUtils.ts index fd3317ee7..fcd72dd65 100644 --- a/src/docdb/utils/rbacUtils.ts +++ b/src/docdb/utils/rbacUtils.ts @@ -7,10 +7,12 @@ import { type SqlRoleAssignmentCreateUpdateParameters } from '@azure/arm-cosmosd import { getResourceGroupFromId } from '@microsoft/vscode-azext-azureutils'; import { callWithTelemetryAndErrorHandling, + createSubscriptionContext, type IActionContext, type IAzureMessageOptions, type ISubscriptionContext, } from '@microsoft/vscode-azext-utils'; +import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; import { randomUUID } from 'crypto'; import * as vscode from 'vscode'; import { createCosmosDBClient } from '../../utils/azureClients'; @@ -46,6 +48,39 @@ export async function ensureRbacPermission(docDbItem: DocDBAccountTreeItemBase, ); } +export async function ensureRbacPermissionV2( + fullId: string, + subscription: AzureSubscription, + principalId: string, +): Promise { + return ( + (await callWithTelemetryAndErrorHandling('cosmosDB.addMissingRbacRole', async (context: IActionContext) => { + context.errorHandling.suppressDisplay = false; + context.errorHandling.rethrow = false; + + const subscriptionContext = createSubscriptionContext(subscription); + const accountName: string = getDatabaseAccountNameFromId(fullId); + if (await askForRbacPermissions(accountName, subscriptionContext.subscriptionDisplayName, context)) { + context.telemetry.properties.lastStep = 'addRbacContributorPermission'; + const resourceGroup: string = getResourceGroupFromId(fullId); + const start: number = Date.now(); + await addRbacContributorPermission( + accountName, + principalId, + resourceGroup, + context, + subscriptionContext, + ); + //send duration of the previous call (in seconds) in addition to the duration of the whole event including user prompt + context.telemetry.measurements['createRoleAssignment'] = (Date.now() - start) / 1000; + + return true; + } + return false; + })) ?? false + ); +} + export function isRbacException(error: Error): boolean { return ( error instanceof Error && error.message.includes('does not have required RBAC permissions to perform action') diff --git a/src/extension.ts b/src/extension.ts index 5e664d6b7..c0a4c3ca8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -60,7 +60,12 @@ import { DatabaseWorkspaceProvider } from './resolver/DatabaseWorkspaceProvider' import { TableAccountTreeItem } from './table/tree/TableAccountTreeItem'; import { AttachedAccountSuffix } from './tree/AttachedAccountsTreeItem'; import { CosmosDBBranchDataProvider } from './tree/CosmosDBBranchDataProvider'; +import { CosmosDBWorkspaceBranchDataProvider } from './tree/CosmosDBWorkspaceBranchDataProvider'; import { SubscriptionTreeItem } from './tree/SubscriptionTreeItem'; +import { + SharedWorkspaceResourceProvider, + WorkspaceResourceType, +} from './tree/workspace/SharedWorkspaceResourceProvider'; import { localize } from './utils/localize'; const cosmosDBTopLevelContextValues: string[] = [ @@ -102,6 +107,11 @@ export async function activateInternal( AzExtResourceType.AzureCosmosDb, new CosmosDBBranchDataProvider(), ); + ext.rgApiV2.resources.registerWorkspaceResourceProvider(new SharedWorkspaceResourceProvider()); + ext.rgApiV2.resources.registerWorkspaceResourceBranchDataProvider( + WorkspaceResourceType.AttachedAccounts, + new CosmosDBWorkspaceBranchDataProvider(), + ); ext.rgApi.registerApplicationResourceResolver( AzExtResourceType.PostgresqlServersStandard, diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 2ad559b6f..9cce4a590 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -23,7 +23,7 @@ import { type PostgresCodeLensProvider } from './postgres/services/PostgresCodeL import { type PostgresDatabaseTreeItem } from './postgres/tree/PostgresDatabaseTreeItem'; import { type AttachedAccountsTreeItem } from './tree/AttachedAccountsTreeItem'; import { type AzureAccountTreeItemWithAttached } from './tree/AzureAccountTreeItemWithAttached'; -import { type SharedWorkspaceResourceProvider } from './tree/workspace/sharedWorkspaceResourceProvider'; +import { type SharedWorkspaceResourceProvider } from './tree/workspace/SharedWorkspaceResourceProvider'; /** * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts diff --git a/src/mongoClusters/MongoClustersExtension.ts b/src/mongoClusters/MongoClustersExtension.ts index d67cb1861..94faa2827 100644 --- a/src/mongoClusters/MongoClustersExtension.ts +++ b/src/mongoClusters/MongoClustersExtension.ts @@ -18,10 +18,7 @@ import { import { AzExtResourceType } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; import { ext } from '../extensionVariables'; -import { - SharedWorkspaceResourceProvider, - WorkspaceResourceType, -} from '../tree/workspace/sharedWorkspaceResourceProvider'; +import { WorkspaceResourceType } from '../tree/workspace/SharedWorkspaceResourceProvider'; import { addWorkspaceConnection } from './commands/addWorkspaceConnection'; import { createCollection } from './commands/createCollection'; import { createDatabase } from './commands/createDatabase'; @@ -71,8 +68,9 @@ export class MongoClustersExtension implements vscode.Disposable { ext.mongoClustersBranchDataProvider, ); - ext.workspaceDataProvider = new SharedWorkspaceResourceProvider(); - ext.rgApiV2.resources.registerWorkspaceResourceProvider(ext.workspaceDataProvider); + // Moved to extension.ts + // ext.workspaceDataProvider = new SharedWorkspaceResourceProvider(); + // ext.rgApiV2.resources.registerWorkspaceResourceProvider(ext.workspaceDataProvider); ext.mongoClustersWorkspaceBranchDataProvider = new MongoClustersWorkspaceBranchDataProvider(); ext.rgApiV2.resources.registerWorkspaceResourceBranchDataProvider( diff --git a/src/mongoClusters/commands/addWorkspaceConnection.ts b/src/mongoClusters/commands/addWorkspaceConnection.ts index 31ac2ce5c..d2b8e543d 100644 --- a/src/mongoClusters/commands/addWorkspaceConnection.ts +++ b/src/mongoClusters/commands/addWorkspaceConnection.ts @@ -8,8 +8,8 @@ import ConnectionString from 'mongodb-connection-string-url'; import * as vscode from 'vscode'; import { API } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; -import { WorkspaceResourceType } from '../../tree/workspace/sharedWorkspaceResourceProvider'; -import { SharedWorkspaceStorage } from '../../tree/workspace/sharedWorkspaceStorage'; +import { WorkspaceResourceType } from '../../tree/workspace/SharedWorkspaceResourceProvider'; +import { SharedWorkspaceStorage } from '../../tree/workspace/SharedWorkspaceStorage'; import { showConfirmationAsInSettings } from '../../utils/dialogs/showConfirmation'; import { localize } from '../../utils/localize'; import { type AddWorkspaceConnectionContext } from '../wizards/addWorkspaceConnection/AddWorkspaceConnectionContext'; diff --git a/src/mongoClusters/commands/removeWorkspaceConnection.ts b/src/mongoClusters/commands/removeWorkspaceConnection.ts index 123299ed3..18ed126d5 100644 --- a/src/mongoClusters/commands/removeWorkspaceConnection.ts +++ b/src/mongoClusters/commands/removeWorkspaceConnection.ts @@ -5,8 +5,8 @@ import { type IActionContext } from '@microsoft/vscode-azext-utils'; import { ext } from '../../extensionVariables'; -import { WorkspaceResourceType } from '../../tree/workspace/sharedWorkspaceResourceProvider'; -import { SharedWorkspaceStorage } from '../../tree/workspace/sharedWorkspaceStorage'; +import { WorkspaceResourceType } from '../../tree/workspace/SharedWorkspaceResourceProvider'; +import { SharedWorkspaceStorage } from '../../tree/workspace/SharedWorkspaceStorage'; import { showConfirmationAsInSettings } from '../../utils/dialogs/showConfirmation'; import { localize } from '../../utils/localize'; import { type MongoClusterWorkspaceItem } from '../tree/workspace/MongoClusterWorkspaceItem'; diff --git a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts index e29c37760..a7b9163b7 100644 --- a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts @@ -5,8 +5,8 @@ import { createGenericElement, type TreeElementBase } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { WorkspaceResourceType } from '../../../tree/workspace/sharedWorkspaceResourceProvider'; -import { SharedWorkspaceStorage } from '../../../tree/workspace/sharedWorkspaceStorage'; +import { WorkspaceResourceType } from '../../../tree/workspace/SharedWorkspaceResourceProvider'; +import { SharedWorkspaceStorage } from '../../../tree/workspace/SharedWorkspaceStorage'; import { type MongoClusterModel } from '../MongoClusterModel'; import { MongoClusterWorkspaceItem } from './MongoClusterWorkspaceItem'; diff --git a/src/tree/AttachedAccountsTreeItem.ts b/src/tree/AttachedAccountsTreeItem.ts index e815f397b..894bee0bc 100644 --- a/src/tree/AttachedAccountsTreeItem.ts +++ b/src/tree/AttachedAccountsTreeItem.ts @@ -34,7 +34,7 @@ import { localize } from '../utils/localize'; import { nonNullProp, nonNullValue } from '../utils/nonNull'; import { SubscriptionTreeItem } from './SubscriptionTreeItem'; -interface IPersistedAccount { +export interface IPersistedAccount { id: string; // defaultExperience is not the same as API but we can't change the name due to backwards compatibility defaultExperience: API; diff --git a/src/tree/CosmosAccountResourceItemBase.ts b/src/tree/CosmosAccountResourceItemBase.ts index 8cadd1ce1..31ffd7ee2 100644 --- a/src/tree/CosmosAccountResourceItemBase.ts +++ b/src/tree/CosmosAccountResourceItemBase.ts @@ -7,10 +7,11 @@ import * as vscode from 'vscode'; import { type TreeItem } from 'vscode'; import { getExperienceLabel, tryGetExperience } from '../AzureDBExperiences'; import { type CosmosAccountModel } from './CosmosAccountModel'; -import { type CosmosDbTreeElement } from './CosmosDbTreeElement'; +import { type CosmosDBTreeElement } from './CosmosDBTreeElement'; -export abstract class CosmosAccountResourceItemBase implements CosmosDbTreeElement { +export abstract class CosmosAccountResourceItemBase implements CosmosDBTreeElement { public id: string; + public contextValue: string = 'cosmosDB.item.account'; protected constructor(protected readonly account: CosmosAccountModel) { this.id = account.id ?? ''; @@ -20,7 +21,7 @@ export abstract class CosmosAccountResourceItemBase implements CosmosDbTreeEleme * Returns the children of the cluster. * @returns The children of the cluster. */ - getChildren(): Promise { + getChildren(): Promise { return Promise.resolve([]); } diff --git a/src/tree/CosmosDBBranchDataProvider.ts b/src/tree/CosmosDBBranchDataProvider.ts index 150b3056e..a40bb489b 100644 --- a/src/tree/CosmosDBBranchDataProvider.ts +++ b/src/tree/CosmosDBBranchDataProvider.ts @@ -11,8 +11,7 @@ import { databaseAccountType } from '../constants'; import { ext } from '../extensionVariables'; import { nonNullProp } from '../utils/nonNull'; import { type CosmosAccountModel, type CosmosDBResource } from './CosmosAccountModel'; -import { type CosmosDbTreeElement } from './CosmosDbTreeElement'; -import { DocumentDBAccountResourceItem } from './DocumentDBAccountResourceItem'; +import { type CosmosDBTreeElement } from './CosmosDBTreeElement'; import { GraphAccountResourceItem } from './graph/GraphAccountResourceItem'; import { type MongoAccountModel } from './mongo/MongoAccountModel'; import { MongoAccountResourceItem } from './mongo/MongoAccountResourceItem'; @@ -21,22 +20,22 @@ import { TableAccountResourceItem } from './table/TableAccountResourceItem'; export class CosmosDBBranchDataProvider extends vscode.Disposable - implements BranchDataProvider + implements BranchDataProvider { - private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter(); + private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter(); constructor() { super(() => this.onDidChangeTreeDataEmitter.dispose()); } - get onDidChangeTreeData(): vscode.Event { + get onDidChangeTreeData(): vscode.Event { return this.onDidChangeTreeDataEmitter.event; } /** * This function is called for every element in the tree when expanding, the element being expanded is being passed as an argument */ - async getChildren(element: CosmosDbTreeElement): Promise { + async getChildren(element: CosmosDBTreeElement): Promise { const result = await callWithTelemetryAndErrorHandling( 'CosmosDBBranchDataProvider.getChildren', async (context: IActionContext) => { @@ -45,9 +44,9 @@ export class CosmosDBBranchDataProvider context.telemetry.properties.parentContext = elementTreeItem.contextValue ?? 'unknown'; return (await element.getChildren?.())?.map((child) => { - return ext.state.wrapItemInStateHandling(child, (child: CosmosDbTreeElement) => + return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => this.refresh(child), - ) as CosmosDbTreeElement; + ) as CosmosDBTreeElement; }); }, ); @@ -59,7 +58,7 @@ export class CosmosDBBranchDataProvider * This function is being called when the resource tree is being built, it is called for every top level of resources. * @param resource */ - async getResourceItem(resource: CosmosDBResource): Promise { + async getResourceItem(resource: CosmosDBResource): Promise { const resourceItem = await callWithTelemetryAndErrorHandling( 'CosmosDBBranchDataProvider.getResourceItem', async (context: IActionContext) => { @@ -79,7 +78,7 @@ export class CosmosDBBranchDataProvider } if (experience?.api === API.Cassandra) { - return new DocumentDBAccountResourceItem(accountModel, experience); + return new NoSqlAccountResourceItem(accountModel, experience); } if (experience?.api === API.Core) { @@ -99,24 +98,24 @@ export class CosmosDBBranchDataProvider // Unknown resource type } - return null as unknown as CosmosDbTreeElement; + return null as unknown as CosmosDBTreeElement; }, ); if (resourceItem) { - return ext.state.wrapItemInStateHandling(resourceItem, (item: CosmosDbTreeElement) => + return ext.state.wrapItemInStateHandling(resourceItem, (item: CosmosDBTreeElement) => this.refresh(item), - ) as CosmosDbTreeElement; + ) as CosmosDBTreeElement; } - return null as unknown as CosmosDbTreeElement; + return null as unknown as CosmosDBTreeElement; } - async getTreeItem(element: CosmosDbTreeElement): Promise { + async getTreeItem(element: CosmosDBTreeElement): Promise { return element.getTreeItem(); } - refresh(element?: CosmosDbTreeElement): void { + refresh(element?: CosmosDBTreeElement): void { this.onDidChangeTreeDataEmitter.fire(element); } } diff --git a/src/tree/CosmosDbTreeElement.ts b/src/tree/CosmosDBTreeElement.ts similarity index 92% rename from src/tree/CosmosDbTreeElement.ts rename to src/tree/CosmosDBTreeElement.ts index aa1f84516..be2ddd16c 100644 --- a/src/tree/CosmosDbTreeElement.ts +++ b/src/tree/CosmosDBTreeElement.ts @@ -11,4 +11,4 @@ export interface ExtTreeElementBase extends TreeElementWithId { getTreeItem(): vscode.TreeItem | Thenable; } -export type CosmosDbTreeElement = ExtTreeElementBase; +export type CosmosDBTreeElement = ExtTreeElementBase; diff --git a/src/tree/CosmosDBWorkspaceBranchDataProvider.ts b/src/tree/CosmosDBWorkspaceBranchDataProvider.ts new file mode 100644 index 000000000..6826ac2ed --- /dev/null +++ b/src/tree/CosmosDBWorkspaceBranchDataProvider.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; +import * as vscode from 'vscode'; +import { ext } from '../extensionVariables'; +import { type CosmosDBResource } from './CosmosAccountModel'; +import { type CosmosDBTreeElement } from './CosmosDBTreeElement'; +import { CosmosDBAttachedAccountsResourceItem } from './attached/CosmosDBAttachedAccountsResourceItem'; + +export class CosmosDBWorkspaceBranchDataProvider + extends vscode.Disposable + implements BranchDataProvider +{ + private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter(); + + constructor() { + super(() => this.onDidChangeTreeDataEmitter.dispose()); + } + + get onDidChangeTreeData(): vscode.Event { + return this.onDidChangeTreeDataEmitter.event; + } + + /** + * This function is called for every element in the tree when expanding, the element being expanded is being passed as an argument + */ + async getChildren(element: CosmosDBTreeElement): Promise { + const result = await callWithTelemetryAndErrorHandling( + 'CosmosDBWorkspaceBranchDataProvider.getChildren', + async (context: IActionContext) => { + const elementTreeItem = await element.getTreeItem(); + + context.telemetry.properties.view = 'workspace'; + context.telemetry.properties.parentContext = elementTreeItem.contextValue ?? 'unknown'; + + return (await element.getChildren?.())?.map((child) => { + return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => + this.refresh(child), + ) as CosmosDBTreeElement; + }); + }, + ); + + return result ?? []; + } + + /** + * This function is being called when the resource tree is being built, it is called for every top level of resources. + */ + async getResourceItem(): Promise { + const resourceItem = await callWithTelemetryAndErrorHandling( + 'CosmosDBWorkspaceBranchDataProvider.getResourceItem', + () => new CosmosDBAttachedAccountsResourceItem(), + ); + + if (resourceItem) { + return ext.state.wrapItemInStateHandling(resourceItem, (item: CosmosDBTreeElement) => + this.refresh(item), + ) as CosmosDBTreeElement; + } + + return null as unknown as CosmosDBTreeElement; + } + + async getTreeItem(element: CosmosDBTreeElement): Promise { + return element.getTreeItem(); + } + + refresh(element?: CosmosDBTreeElement): void { + this.onDidChangeTreeDataEmitter.fire(element); + } +} diff --git a/src/tree/DocumentDBAccountResourceItem.ts b/src/tree/DocumentDBAccountResourceItem.ts deleted file mode 100644 index c073841c8..000000000 --- a/src/tree/DocumentDBAccountResourceItem.ts +++ /dev/null @@ -1,114 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type CosmosDBManagementClient, type DatabaseAccountGetResults } from '@azure/arm-cosmosdb'; -import { type DatabaseAccountListKeysResult } from '@azure/arm-cosmosdb/src/models'; -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import vscode from 'vscode'; -import { type Experience } from '../AzureDBExperiences'; -import { type CosmosDBCredential, type CosmosDBKeyCredential } from '../docdb/getCosmosClient'; -import { createCosmosDBManagementClient } from '../utils/azureClients'; -import { localize } from '../utils/localize'; -import { nonNullProp } from '../utils/nonNull'; -import { type CosmosAccountModel } from './CosmosAccountModel'; -import { CosmosAccountResourceItemBase } from './CosmosAccountResourceItemBase'; - -export class DocumentDBAccountResourceItem extends CosmosAccountResourceItemBase { - protected databaseAccount?: DatabaseAccountGetResults; - protected credentials?: CosmosDBCredential[]; - protected documentEndpoint?: string; - - constructor( - account: CosmosAccountModel, - protected experience: Experience, - ) { - super(account); - } - - protected getClient() { - return callWithTelemetryAndErrorHandling( - 'CosmosAccountResourceItemBase.getClient', - async (context: IActionContext) => { - return createCosmosDBManagementClient(context, this.account.subscription); - }, - ); - } - - protected async init(): Promise { - const id = nonNullProp(this.account, 'id'); - const name = nonNullProp(this.account, 'name'); - const resourceGroup = nonNullProp(this.account, 'resourceGroup'); - const client = await this.getClient(); - - if (!client) { - return; - } - - const databaseAccount = await client.databaseAccounts.get(resourceGroup, name); - this.credentials = await this.getCredentials(name, resourceGroup, client, databaseAccount); - this.documentEndpoint = nonNullProp(databaseAccount, 'documentEndpoint', `of the database account ${id}`); - } - - private async getCredentials( - name: string, - resourceGroup: string, - client: CosmosDBManagementClient, - databaseAccount: DatabaseAccountGetResults, - ): Promise { - const result = await callWithTelemetryAndErrorHandling( - 'CosmosDBBranchDataProvider.getCredentials', - async (context: IActionContext) => { - const localAuthDisabled = databaseAccount.disableLocalAuth === true; - const forceOAuth = vscode.workspace.getConfiguration().get('azureDatabases.useCosmosOAuth'); - context.telemetry.properties.useCosmosOAuth = (forceOAuth ?? false).toString(); - - let keyCred: CosmosDBKeyCredential | undefined = undefined; - // disable key auth if the user has opted in to OAuth (AAD/Entra ID) - if (!forceOAuth) { - try { - context.telemetry.properties.localAuthDisabled = localAuthDisabled.toString(); - - let keyResult: DatabaseAccountListKeysResult | undefined; - // If the account has local auth disabled, don't even try to use key auth - if (!localAuthDisabled) { - keyResult = await client.databaseAccounts.listKeys(resourceGroup, name); - keyCred = keyResult?.primaryMasterKey - ? { - type: 'key', - key: keyResult.primaryMasterKey, - } - : undefined; - context.telemetry.properties.receivedKeyCreds = 'true'; - } else { - throw new Error('Local auth is disabled'); - } - } catch { - context.telemetry.properties.receivedKeyCreds = 'false'; - const message = localize( - 'keyPermissionErrorMsg', - 'You do not have the required permissions to list auth keys for [{0}].\nFalling back to using Entra ID.\nYou can change the default authentication in the settings.', - name, - ); - const openSettingsItem = localize('openSettings', 'Open Settings'); - void vscode.window.showWarningMessage(message, ...[openSettingsItem]).then((item) => { - if (item === openSettingsItem) { - void vscode.commands.executeCommand( - 'workbench.action.openSettings', - 'azureDatabases.useCosmosOAuth', - ); - } - }); - } - } - - // OAuth is always enabled for Cosmos DB and will be used as a fallback if key auth is unavailable - const authCred = { type: 'auth' }; - return [keyCred, authCred].filter((cred): cred is CosmosDBCredential => cred !== undefined); - }, - ); - - return result ?? []; - } -} diff --git a/src/tree/graph/GraphAccountModel.ts b/src/tree/attached/CosmosDBAttachedAccountModel.ts similarity index 72% rename from src/tree/graph/GraphAccountModel.ts rename to src/tree/attached/CosmosDBAttachedAccountModel.ts index 050e7f504..f1574b7c0 100644 --- a/src/tree/graph/GraphAccountModel.ts +++ b/src/tree/attached/CosmosDBAttachedAccountModel.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type CosmosAccountModel } from '../CosmosAccountModel'; - -export type GraphAccountModel = CosmosAccountModel; +export type CosmosDBAttachedAccountModel = { + connectionString: string; + id: string; + isEmulator: boolean; + name: string; +}; diff --git a/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts b/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts new file mode 100644 index 000000000..9ed64cb44 --- /dev/null +++ b/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts @@ -0,0 +1,216 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + callWithTelemetryAndErrorHandling, + createGenericElement, + nonNullValue, + type IActionContext, +} from '@microsoft/vscode-azext-utils'; +import vscode, { ThemeIcon, TreeItemCollapsibleState } from 'vscode'; +import { API, getExperienceFromApi } from '../../AzureDBExperiences'; +import { isWindows } from '../../constants'; +import { ext } from '../../extensionVariables'; +import { type IPersistedAccount } from '../AttachedAccountsTreeItem'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { GraphAccountAttachedResourceItem } from '../graph/GraphAccountAttachedResourceItem'; +import { NoSqlAccountAttachedResourceItem } from '../nosql/NoSqlAccountAttachedResourceItem'; +import { TableAccountAttachedResourceItem } from '../table/TableAccountAttachedResourceItem'; +import { WorkspaceResourceType } from '../workspace/SharedWorkspaceResourceProvider'; +import { SharedWorkspaceStorage, type SharedWorkspaceStorageItem } from '../workspace/SharedWorkspaceStorage'; +import { type CosmosDBAttachedAccountModel } from './CosmosDBAttachedAccountModel'; + +export class CosmosDBAttachedAccountsResourceItem implements CosmosDBTreeElement { + public id: string = WorkspaceResourceType.AttachedAccounts; + public contextValue: string = 'cosmosDB.workspace.item.accounts'; + + private readonly attachDatabaseAccount: CosmosDBTreeElement; + private readonly attachEmulator: CosmosDBTreeElement; + + constructor() { + this.attachDatabaseAccount = createGenericElement({ + id: `${this.id}/attachAccount`, + contextValue: `${this.contextValue}/attachAccount`, + label: 'Attach Database Account\u2026', + iconPath: new vscode.ThemeIcon('plus'), + commandId: 'cosmosDB.attachDatabaseAccount', + includeInTreeItemPicker: true, + }) as CosmosDBTreeElement; + + this.attachEmulator = createGenericElement({ + id: `${this.id}/attachEmulator`, + contextValue: `${this.contextValue}/attachEmulator`, + label: 'Attach Emulator\u2026', + iconPath: new vscode.ThemeIcon('plus'), + commandId: 'cosmosDB.attachEmulator', + includeInTreeItemPicker: true, + }) as CosmosDBTreeElement; + } + + public async getChildren(): Promise { + const items = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { + context.telemetry.properties.view = 'workspace'; + context.telemetry.properties.parentContext = this.contextValue; + + // TODO: remove after a few releases + await this.migrateV1AccountsToV2(); // Move accounts from the old storage format to the new one + + const items = await SharedWorkspaceStorage.getItems(this.id); + + return await this.getChildrenImpl(items); + }); + + const auxItems = isWindows ? [this.attachDatabaseAccount, this.attachEmulator] : [this.attachDatabaseAccount]; + + const result: CosmosDBTreeElement[] = []; + result.push(...(items ?? [])); + result.push(...auxItems); + + return result; + } + + public getTreeItem() { + return { + id: this.id, + contextValue: 'cosmosDB.workspace.item.accounts', + label: 'Attached Database Accounts', + iconPath: new ThemeIcon('plug'), + collapsibleState: TreeItemCollapsibleState.Collapsed, + }; + } + + protected async getChildrenImpl(items: SharedWorkspaceStorageItem[]): Promise { + return Promise.resolve( + items + .map((item) => { + const { id, name, properties, secrets } = item; + const api: API = nonNullValue(properties?.api, 'api') as API; + const isEmulator: boolean = !!nonNullValue(properties?.isEmulator, 'isEmulator'); + const connectionString: string = nonNullValue(secrets?.[0], 'connectionString'); + const experience = getExperienceFromApi(api); + const accountModel: CosmosDBAttachedAccountModel = { + id, + name, + connectionString, + isEmulator, + }; + + if (experience?.api === API.Cassandra) { + return new NoSqlAccountAttachedResourceItem(accountModel, experience); + } + + if (experience?.api === API.Core) { + return new NoSqlAccountAttachedResourceItem(accountModel, experience); + } + + if (experience?.api === API.Graph) { + return new GraphAccountAttachedResourceItem(accountModel, experience); + } + + if (experience?.api === API.Table) { + return new TableAccountAttachedResourceItem(accountModel, experience); + } + + // Unknown experience + return undefined; + }) + .filter((r) => r !== undefined), + ); + } + + protected async migrateV1AccountsToV2(): Promise { + const serviceName = 'ms-azuretools.vscode-cosmosdb.connectionStrings'; + const value: string | undefined = ext.context.globalState.get(serviceName); + + if (!value) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const accounts: (string | IPersistedAccount)[] = JSON.parse(value); + const result = await Promise.allSettled( + accounts.map(async (account) => { + return callWithTelemetryAndErrorHandling( + 'CosmosDBAttachedAccountsResourceItem.migrateV1AccountsToV2', + async (context) => { + context.errorHandling.rethrow = true; + context.errorHandling.forceIncludeInReportIssueCommand = true; + + let id: string; + let name: string; + let isEmulator: boolean; + let api: API; + + if (typeof account === 'string') { + // Default to Mongo if the value is a string for the sake of backwards compatibility + // (Mongo was originally the only account type that could be attached) + id = account; + name = account; + api = API.MongoDB; + isEmulator = false; + } else { + id = (account).id; + name = (account).id; + api = (account).defaultExperience; + isEmulator = (account).isEmulator ?? false; + } + + const connectionString: string = nonNullValue( + await ext.secretStorage.get(`${serviceName}.${id}`), + 'connectionString', + ); + + const storageItem: SharedWorkspaceStorageItem = { + id, + name, + properties: { + isEmulator, + api, + }, + secrets: [connectionString], + }; + + await SharedWorkspaceStorage.push(WorkspaceResourceType.AttachedAccounts, storageItem); + await ext.secretStorage.delete(`${serviceName}.${id}`); + + return storageItem; + }, + ); + }), + ); + + const notMovedAccounts = result + .map((r, index) => { + if (r.status === 'rejected') { + // Couldn't migrate the account, won't remove it from the old list + return accounts[index]; + } + + const storageItem = r.value; + + if (storageItem?.properties?.api === API.MongoDB) { + // TODO: Tomasz will handle this + return accounts[index]; + } + + if ( + storageItem?.properties?.api === API.PostgresSingle || + storageItem?.properties?.api === API.PostgresFlexible + ) { + // TODO: Need to handle Postgres + return accounts[index]; + } + + return undefined; + }) + .filter((r) => r !== undefined); + + if (notMovedAccounts.length > 0) { + await ext.context.globalState.update(serviceName, JSON.stringify(notMovedAccounts)); + } else { + await ext.context.globalState.update(serviceName, undefined); + } + } +} diff --git a/src/tree/nosql/NoSqlAccountModel.ts b/src/tree/docdb/AccountInfo.ts similarity index 61% rename from src/tree/nosql/NoSqlAccountModel.ts rename to src/tree/docdb/AccountInfo.ts index cca4a39a9..ba8691ea0 100644 --- a/src/tree/nosql/NoSqlAccountModel.ts +++ b/src/tree/docdb/AccountInfo.ts @@ -3,6 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type CosmosAccountModel } from '../CosmosAccountModel'; +import { type CosmosDBCredential } from '../../docdb/getCosmosClient'; -export type NoSqlAccountModel = CosmosAccountModel; +export interface AccountInfo { + credentials: CosmosDBCredential[]; + endpoint: string; + id: string; + isEmulator: boolean; + name: string; +} diff --git a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts new file mode 100644 index 000000000..625202e1c --- /dev/null +++ b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosClient, type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import vscode, { type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { getThemeAgnosticIconPath } from '../../constants'; +import { parseDocDBConnectionString } from '../../docdb/docDBConnectionStrings'; +import { type CosmosDBCredential, type CosmosDBKeyCredential, getCosmosClient } from '../../docdb/getCosmosClient'; +import { getSignedInPrincipalIdForAccountEndpoint } from '../../docdb/utils/azureSessionHelper'; +import { isRbacException, showRbacPermissionError } from '../../docdb/utils/rbacUtils'; +import { type CosmosDBAttachedAccountModel } from '../attached/CosmosDBAttachedAccountModel'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type AccountInfo } from './AccountInfo'; + +export abstract class DocumentDBAccountAttachedResourceItem implements CosmosDBTreeElement { + public id: string; + public contextValue: string = 'cosmosDB.workspace.item.account'; + + // To prevent the RBAC notification from showing up multiple times + protected hasShownRbacNotification: boolean = false; + + protected constructor( + protected account: CosmosDBAttachedAccountModel, + protected experience: Experience, + ) { + this.contextValue = `${experience.api}.workspace.item.account`; + } + + public async getChildren(): Promise { + const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + const accountInfo = await this.getAccountInfo(this.account); + const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); + const databases = await this.getDatabases(accountInfo, cosmosClient); + return await this.getChildrenImpl(accountInfo, databases); + }); + + return result ?? []; + } + + public getTreeItem(): TreeItem { + // This function is a bit easier than the ancestor's getTreeItem function + return { + id: this.id, + contextValue: this.contextValue, + iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg'), + label: this.account.name, + description: `(${this.experience.shortName})`, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + }; + } + + protected async getAccountInfo(account: CosmosDBAttachedAccountModel): Promise | never { + const id = account.id; + const name = account.name; + const isEmulator = account.isEmulator; + const parsedCS = parseDocDBConnectionString(account.connectionString); + const documentEndpoint = parsedCS.documentEndpoint; + const credentials = await this.getCredentials(account.connectionString); + + return { + credentials, + endpoint: documentEndpoint, + id, + isEmulator, + name, + }; + } + + protected async getDatabases( + accountInfo: AccountInfo, + cosmosClient: CosmosClient, + ): Promise<(DatabaseDefinition & Resource)[]> | never { + const getResources = async () => { + const result = await cosmosClient.databases.readAll().fetchAll(); + return result.resources; + }; + + try { + // Await is required here to ensure that the error is caught in the catch block + return await getResources(); + } catch (e) { + if (e instanceof Error && isRbacException(e) && !this.hasShownRbacNotification) { + this.hasShownRbacNotification = true; + + const principalId = (await getSignedInPrincipalIdForAccountEndpoint(accountInfo.endpoint)) ?? ''; + void showRbacPermissionError(this.id, principalId); + } + throw e; // rethrowing tells the resources extension to show the exception message in the tree + } + } + + protected async getCredentials(connectionString: string): Promise { + const result = await callWithTelemetryAndErrorHandling('getCredentials', async (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + const forceOAuth = vscode.workspace.getConfiguration().get('azureDatabases.useCosmosOAuth'); + context.telemetry.properties.useCosmosOAuth = (forceOAuth ?? false).toString(); + + let keyCred: CosmosDBKeyCredential | undefined = undefined; + // disable key auth if the user has opted in to OAuth (AAD/Entra ID) + if (!forceOAuth) { + const parsedCS = parseDocDBConnectionString(connectionString); + keyCred = { + type: 'key', + key: parsedCS.masterKey, + }; + } + + // OAuth is always enabled for Cosmos DB and will be used as a fallback if key auth is unavailable + const authCred = { type: 'auth' }; + return [keyCred, authCred].filter((cred): cred is CosmosDBCredential => cred !== undefined); + }); + + return result ?? []; + } + + protected abstract getChildrenImpl( + accountInfo: AccountInfo, + databases: (DatabaseDefinition & Resource)[], + ): Promise; +} diff --git a/src/tree/docdb/DocumentDBAccountResourceItem.ts b/src/tree/docdb/DocumentDBAccountResourceItem.ts new file mode 100644 index 000000000..5a5bf8418 --- /dev/null +++ b/src/tree/docdb/DocumentDBAccountResourceItem.ts @@ -0,0 +1,178 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosDBManagementClient, type DatabaseAccountGetResults } from '@azure/arm-cosmosdb'; +import { type DatabaseAccountListKeysResult } from '@azure/arm-cosmosdb/src/models'; +import { type CosmosClient, type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import vscode, { type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBCredential, type CosmosDBKeyCredential, getCosmosClient } from '../../docdb/getCosmosClient'; +import { getSignedInPrincipalIdForAccountEndpoint } from '../../docdb/utils/azureSessionHelper'; +import { ensureRbacPermissionV2, isRbacException, showRbacPermissionError } from '../../docdb/utils/rbacUtils'; +import { createCosmosDBManagementClient } from '../../utils/azureClients'; +import { localize } from '../../utils/localize'; +import { nonNullProp } from '../../utils/nonNull'; +import { type CosmosAccountModel } from '../CosmosAccountModel'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type AccountInfo } from './AccountInfo'; + +export abstract class DocumentDBAccountResourceItem implements CosmosDBTreeElement { + public id: string; + public contextValue: string = 'cosmosDB.item.account'; + + // To prevent the RBAC notification from showing up multiple times + protected hasShownRbacNotification: boolean = false; + + protected constructor( + protected account: CosmosAccountModel, + protected experience: Experience, + ) { + this.contextValue = `${experience.api}.item.account`; + } + + public async getChildren(): Promise { + const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + const accountInfo = await this.getAccountInfo(context, this.account); + const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); + const databases = await this.getDatabases(accountInfo, cosmosClient); + return await this.getChildrenImpl(accountInfo, databases); + }); + + return result ?? []; + } + + public getTreeItem(): TreeItem { + // This function is a bit easier than the ancestor's getTreeItem function + return { + id: this.id, + contextValue: this.contextValue, + label: this.account.name, + description: `(${this.experience.shortName})`, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + }; + } + + protected async getAccountInfo(context: IActionContext, account: CosmosAccountModel): Promise | never { + const id = nonNullProp(account, 'id'); + const name = nonNullProp(account, 'name'); + const resourceGroup = nonNullProp(account, 'resourceGroup'); + const client = await createCosmosDBManagementClient(context, account.subscription); + + const databaseAccount = await client.databaseAccounts.get(resourceGroup, name); + const credentials = await this.getCredentials(name, resourceGroup, client, databaseAccount); + const documentEndpoint = nonNullProp(databaseAccount, 'documentEndpoint', `of the database account ${id}`); + + return { + credentials, + endpoint: documentEndpoint, + id, + isEmulator: false, + name, + }; + } + + protected async getDatabases( + accountInfo: AccountInfo, + cosmosClient: CosmosClient, + ): Promise<(DatabaseDefinition & Resource)[]> | never { + const getResources = async () => { + const result = await cosmosClient.databases.readAll().fetchAll(); + return result.resources; + }; + + try { + // Await is required here to ensure that the error is caught in the catch block + return await getResources(); + } catch (e) { + if (e instanceof Error && isRbacException(e) && !this.hasShownRbacNotification) { + this.hasShownRbacNotification = true; + + const principalId = (await getSignedInPrincipalIdForAccountEndpoint(accountInfo.endpoint)) ?? ''; + // check if the principal ID matches the one that is signed in, + // otherwise this might be a security problem, hence show the error message + if ( + e.message.includes(`[${principalId}]`) && + (await ensureRbacPermissionV2(this.id, this.account.subscription, principalId)) + ) { + return getResources(); + } else { + void showRbacPermissionError(this.id, principalId); + } + } + throw e; // rethrowing tells the resources extension to show the exception message in the tree + } + } + + protected async getCredentials( + name: string, + resourceGroup: string, + client: CosmosDBManagementClient, + databaseAccount: DatabaseAccountGetResults, + ): Promise { + const result = await callWithTelemetryAndErrorHandling('getCredentials', async (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + const localAuthDisabled = databaseAccount.disableLocalAuth === true; + const forceOAuth = vscode.workspace.getConfiguration().get('azureDatabases.useCosmosOAuth'); + context.telemetry.properties.useCosmosOAuth = (forceOAuth ?? false).toString(); + + let keyCred: CosmosDBKeyCredential | undefined = undefined; + // disable key auth if the user has opted in to OAuth (AAD/Entra ID) + if (!forceOAuth) { + try { + context.telemetry.properties.localAuthDisabled = localAuthDisabled.toString(); + + let keyResult: DatabaseAccountListKeysResult | undefined; + // If the account has local auth disabled, don't even try to use key auth + if (!localAuthDisabled) { + keyResult = await client.databaseAccounts.listKeys(resourceGroup, name); + keyCred = keyResult?.primaryMasterKey + ? { + type: 'key', + key: keyResult.primaryMasterKey, + } + : undefined; + context.telemetry.properties.receivedKeyCreds = 'true'; + } else { + throw new Error('Local auth is disabled'); + } + } catch { + context.telemetry.properties.receivedKeyCreds = 'false'; + + const message = localize( + 'keyPermissionErrorMsg', + 'You do not have the required permissions to list auth keys for [{0}].\nFalling back to using Entra ID.\nYou can change the default authentication in the settings.', + name, + ); + const openSettingsItem = localize('openSettings', 'Open Settings'); + void vscode.window.showWarningMessage(message, ...[openSettingsItem]).then((item) => { + if (item === openSettingsItem) { + void vscode.commands.executeCommand( + 'workbench.action.openSettings', + 'azureDatabases.useCosmosOAuth', + ); + } + }); + } + } + + // OAuth is always enabled for Cosmos DB and will be used as a fallback if key auth is unavailable + const authCred = { type: 'auth' }; + return [keyCred, authCred].filter((cred): cred is CosmosDBCredential => cred !== undefined); + }); + + return result ?? []; + } + + protected abstract getChildrenImpl( + accountInfo: AccountInfo, + databases: (DatabaseDefinition & Resource)[], + ): Promise; +} diff --git a/src/tree/docdb/DocumentDBContainerResourceItem.ts b/src/tree/docdb/DocumentDBContainerResourceItem.ts new file mode 100644 index 000000000..b3c141653 --- /dev/null +++ b/src/tree/docdb/DocumentDBContainerResourceItem.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { v4 as uuid } from 'uuid'; +import vscode, { type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type DocumentDBContainerModel } from './models/DocumentDBContainerModel'; + +export abstract class DocumentDBContainerResourceItem implements CosmosDBTreeElement { + public id: string; + public contextValue: string = 'cosmosDB.item.container'; + + protected constructor( + protected readonly model: DocumentDBContainerModel, + protected readonly experience: Experience, + ) { + this.id = uuid(); + this.contextValue = `${experience.api}.item.container`; + } + + async getChildren(): Promise { + const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + const triggers = await this.getChildrenTriggersImpl(); + const storedProcedures = await this.getChildrenStoredProceduresImpl(); + const items = await this.getChildrenItemsImpl(); + + return [items, storedProcedures, triggers].filter((r) => r !== undefined); + }); + + return result ?? []; + } + + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: this.contextValue, + iconPath: new vscode.ThemeIcon('files'), + label: this.model.container.id, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + }; + } + + protected abstract getChildrenTriggersImpl(): Promise; + protected abstract getChildrenStoredProceduresImpl(): Promise; + protected abstract getChildrenItemsImpl(): Promise; +} diff --git a/src/tree/docdb/DocumentDBDatabaseResourceItem.ts b/src/tree/docdb/DocumentDBDatabaseResourceItem.ts new file mode 100644 index 000000000..715273ec9 --- /dev/null +++ b/src/tree/docdb/DocumentDBDatabaseResourceItem.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ContainerDefinition, type CosmosClient, type Resource } from '@azure/cosmos'; +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { v4 as uuid } from 'uuid'; +import vscode, { type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { getCosmosClient } from '../../docdb/getCosmosClient'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type DocumentDBDatabaseModel } from './models/DocumentDBDatabaseModel'; + +export abstract class DocumentDBDatabaseResourceItem implements CosmosDBTreeElement { + public id: string; + public contextValue: string = 'cosmosDB.item.database'; + + protected constructor( + protected readonly model: DocumentDBDatabaseModel, + protected readonly experience: Experience, + ) { + this.id = uuid(); + this.contextValue = `${experience.api}.item.database`; + } + + async getChildren(): Promise { + const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + const containers = await this.getContainers(cosmosClient); + + return await this.getChildrenImpl(containers); + }); + + return result ?? []; + } + + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: this.contextValue, + iconPath: new vscode.ThemeIcon('database'), + label: this.model.database.id, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + }; + } + + protected async getContainers(cosmosClient: CosmosClient): Promise<(ContainerDefinition & Resource)[]> { + const result = await cosmosClient.database(this.model.database.id).containers.readAll().fetchAll(); + return result.resources; + } + + protected abstract getChildrenImpl(containers: (ContainerDefinition & Resource)[]): Promise; +} diff --git a/src/tree/docdb/DocumentDBItemResourceItem.ts b/src/tree/docdb/DocumentDBItemResourceItem.ts new file mode 100644 index 000000000..ea06ccd17 --- /dev/null +++ b/src/tree/docdb/DocumentDBItemResourceItem.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { v4 as uuid } from 'uuid'; +import vscode, { type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { extractPartitionKey, getDocumentId } from '../../utils/document'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type DocumentDBItemModel } from './models/DocumentDBItemModel'; + +export abstract class DocumentDBItemResourceItem implements CosmosDBTreeElement { + public id: string; + public contextValue: string = 'cosmosDB.item.item'; + + protected constructor( + protected readonly model: DocumentDBItemModel, + protected readonly experience: Experience, + ) { + // Generate a unique ID for the item + // This is used to identify the item in the tree, not the item itself + // The item id is not guaranteed to be unique + this.id = uuid(); + this.contextValue = `${experience.api}.item.item`; + } + + getTreeItem(): TreeItem { + const documentId = getDocumentId(this.model.item, this.model.container.partitionKey); + return { + id: this.id, + contextValue: this.contextValue, + iconPath: new vscode.ThemeIcon('file'), + label: documentId?.id ?? documentId?._rid ?? '', + tooltip: new vscode.MarkdownString( + `${this.generateDocumentTooltip()}\n${this.generatePartitionKeyTooltip()}`, + ), + collapsibleState: vscode.TreeItemCollapsibleState.None, + command: { + title: 'Open Document', + command: 'cosmosDB.openDocument', + }, + }; + } + + protected generateDocumentTooltip(): string { + return ( + '### Document\n' + + '---\n' + + `${this.model.item.id ? `- ID: **${this.model.item.id}**\n` : ''}` + + `${this.model.item._id ? `- ID (_id): **${this.model.item._id}**\n` : ''}` + + `${this.model.item._rid ? `- RID: **${this.model.item._rid}**\n` : ''}` + + `${this.model.item._self ? `- Self Link: **${this.model.item._self}**\n` : ''}` + + `${this.model.item._etag ? `- ETag: **${this.model.item._etag}**\n` : ''}` + + `${this.model.item._ts ? `- Timestamp: **${this.model.item._ts}**\n` : ''}` + ); + } + + protected generatePartitionKeyTooltip(): string { + if (!this.model.container.partitionKey || this.model.container.partitionKey.paths.length === 0) { + return ''; + } + const partitionKeyPaths = this.model.container.partitionKey.paths.join(', '); + let partitionKeyValues = extractPartitionKey(this.model.item, this.model.container.partitionKey); + partitionKeyValues = Array.isArray(partitionKeyValues) ? partitionKeyValues : [partitionKeyValues]; + partitionKeyValues = partitionKeyValues.map((v) => { + if (v === null) { + return '\\'; + } + if (v === undefined) { + return '\\'; + } + if (typeof v === 'object') { + return JSON.stringify(v); + } + return v; + }); + + return ( + '### Partition Key\n' + + '---\n' + + `- Paths: **${partitionKeyPaths}**\n` + + `- Values: **${partitionKeyValues.join(', ')}**\n` + ); + } +} diff --git a/src/tree/docdb/DocumentDBItemsResourceItem.ts b/src/tree/docdb/DocumentDBItemsResourceItem.ts new file mode 100644 index 000000000..b33800f0f --- /dev/null +++ b/src/tree/docdb/DocumentDBItemsResourceItem.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosClient, type FeedOptions, type ItemDefinition, type QueryIterator } from '@azure/cosmos'; +import { + callWithTelemetryAndErrorHandling, + createGenericElement, + type IActionContext, +} from '@microsoft/vscode-azext-utils'; +import { v4 as uuid } from 'uuid'; +import vscode, { type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { getCosmosClient } from '../../docdb/getCosmosClient'; +import { getBatchSizeSetting } from '../../utils/workspacUtils'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type DocumentDBItemsModel } from './models/DocumentDBItemsModel'; + +export abstract class DocumentDBItemsResourceItem implements CosmosDBTreeElement { + public id: string; + public contextValue: string = 'cosmosDB.item.items'; + + protected iterator: QueryIterator | undefined; + protected cachedItems: ItemDefinition[] = []; + protected hasMoreChildren: boolean = true; + + protected constructor( + protected readonly model: DocumentDBItemsModel, + protected readonly experience: Experience, + ) { + this.id = uuid(); + this.contextValue = `${experience.api}.item.items`; + } + + public async getChildren(): Promise { + const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + if (this.iterator && this.cachedItems.length > 0) { + // ignore + } else { + // Fetch the first batch + const batchSize = getBatchSizeSetting(); + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + + this.iterator = this.getIterator(cosmosClient, { maxItemCount: batchSize }); + + await this.getItems(this.iterator); + } + + return await this.getChildrenImpl(this.cachedItems); + }); + + if (result && this.hasMoreChildren) { + result.push( + createGenericElement({ + contextValue: this.contextValue, + iconPath: new vscode.ThemeIcon('refresh'), + label: 'Load more\u2026', + id: `${this.id}/loadMore`, + commandId: 'cosmosDB.loadMore', + commandArgs: [ + this.id, + (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + if (this.iterator) { + return this.getItems(this.iterator); + // Then refresh the tree + } else { + return []; + } + }, + ], + }) as CosmosDBTreeElement, + ); + } + + return result ?? []; + } + + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: this.contextValue, + iconPath: new vscode.ThemeIcon('files'), + label: 'Documents', + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + }; + } + + protected getIterator(cosmosClient: CosmosClient, feedOptions: FeedOptions): QueryIterator { + return cosmosClient + .database(this.model.database.id) + .container(this.model.container.id) + .items.readAll(feedOptions); + } + + protected async getItems(iterator: QueryIterator): Promise { + const result = await iterator.fetchNext(); + const items = result.resources; + this.hasMoreChildren = result.hasMoreResults; + this.cachedItems.push(...items); + + return items; + } + + protected abstract getChildrenImpl(items: ItemDefinition[]): Promise; +} diff --git a/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts b/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts new file mode 100644 index 000000000..67dcfacd5 --- /dev/null +++ b/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { v4 as uuid } from 'uuid'; +import vscode, { type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type DocumentDBStoredProcedureModel } from './models/DocumentDBStoredProcedureModel'; + +export abstract class DocumentDBStoredProcedureResourceItem implements CosmosDBTreeElement { + public id: string; + public contextValue: string = 'cosmosDB.item.storedProcedure'; + + protected constructor( + protected readonly model: DocumentDBStoredProcedureModel, + protected readonly experience: Experience, + ) { + this.id = uuid(); + this.contextValue = `${experience.api}.item.storedProcedure`; + } + + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: this.contextValue, + iconPath: new vscode.ThemeIcon('server-process'), + label: this.model.procedure.id, + collapsibleState: vscode.TreeItemCollapsibleState.None, + command: { + title: 'Open Stored Procedure', + command: 'cosmosDB.openStoredProcedure', + }, + }; + } +} diff --git a/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts b/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts new file mode 100644 index 000000000..3f49522cd --- /dev/null +++ b/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosClient, type Resource, type StoredProcedureDefinition } from '@azure/cosmos'; +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { v4 as uuid } from 'uuid'; +import vscode, { type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { getCosmosClient } from '../../docdb/getCosmosClient'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type DocumentDBStoredProceduresModel } from './models/DocumentDBStoredProceduresModel'; + +export abstract class DocumentDBStoredProceduresResourceItem implements CosmosDBTreeElement { + public id: string; + public contextValue: string = 'cosmosDB.item.storedProcedures'; + + protected constructor( + protected readonly model: DocumentDBStoredProceduresModel, + protected readonly experience: Experience, + ) { + this.id = uuid(); + this.contextValue = `${experience.api}.item.storedProcedures`; + } + + public async getChildren(): Promise { + const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + const storedProcedures = await this.getStoredProcedures(cosmosClient); + + return await this.getChildrenImpl(storedProcedures); + }); + + return result ?? []; + } + + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: this.contextValue, + iconPath: new vscode.ThemeIcon('server-process'), + label: 'StoredProcedures', + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + }; + } + + protected async getStoredProcedures(cosmosClient: CosmosClient): Promise<(StoredProcedureDefinition & Resource)[]> { + const result = await cosmosClient + .database(this.model.database.id) + .container(this.model.container.id) + .scripts.storedProcedures.readAll() + .fetchAll(); + + return result.resources; + } + + protected abstract getChildrenImpl( + storedProcedures: (StoredProcedureDefinition & Resource)[], + ): Promise; +} diff --git a/src/tree/docdb/DocumentDBTriggerResourceItem.ts b/src/tree/docdb/DocumentDBTriggerResourceItem.ts new file mode 100644 index 000000000..e7f885418 --- /dev/null +++ b/src/tree/docdb/DocumentDBTriggerResourceItem.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { v4 as uuid } from 'uuid'; +import vscode, { type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type DocumentDBTriggerModel } from './models/DocumentDBTriggerModel'; + +export abstract class DocumentDBTriggerResourceItem implements CosmosDBTreeElement { + public id: string; + public contextValue: string = 'cosmosDB.item.trigger'; + + protected constructor( + protected readonly model: DocumentDBTriggerModel, + protected readonly experience: Experience, + ) { + this.id = uuid(); + this.contextValue = `${experience.api}.item.trigger`; + } + + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: this.contextValue, + iconPath: new vscode.ThemeIcon('zap'), + label: this.model.trigger.id, + collapsibleState: vscode.TreeItemCollapsibleState.None, + command: { + title: 'Open Trigger', + command: 'cosmosDB.openTrigger', + }, + }; + } +} diff --git a/src/tree/docdb/DocumentDBTriggersResourceItem.ts b/src/tree/docdb/DocumentDBTriggersResourceItem.ts new file mode 100644 index 000000000..fd246f3bf --- /dev/null +++ b/src/tree/docdb/DocumentDBTriggersResourceItem.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type CosmosClient, type Resource, type TriggerDefinition } from '@azure/cosmos'; +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { v4 as uuid } from 'uuid'; +import vscode, { type TreeItem } from 'vscode'; +import { type Experience } from '../../AzureDBExperiences'; +import { getCosmosClient } from '../../docdb/getCosmosClient'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type DocumentDBTriggersModel } from './models/DocumentDBTriggersModel'; + +export abstract class DocumentDBTriggersResourceItem implements CosmosDBTreeElement { + public id: string; + public contextValue: string = 'cosmosDB.item.triggers'; + + protected constructor( + protected readonly model: DocumentDBTriggersModel, + protected readonly experience: Experience, + ) { + this.id = uuid(); + this.contextValue = `${experience.api}.item.triggers`; + } + + public async getChildren(): Promise { + const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + const triggers = await this.getTriggers(cosmosClient); + + return await this.getChildrenImpl(triggers); + }); + + return result ?? []; + } + + getTreeItem(): TreeItem { + return { + id: this.id, + contextValue: this.contextValue, + iconPath: new vscode.ThemeIcon('zap'), + label: 'Triggers', + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + }; + } + + protected async getTriggers(cosmosClient: CosmosClient): Promise<(TriggerDefinition & Resource)[]> { + const result = await cosmosClient + .database(this.model.database.id) + .container(this.model.container.id) + .scripts.triggers.readAll() + .fetchAll(); + return result.resources; + } + + protected abstract getChildrenImpl(triggers: (TriggerDefinition & Resource)[]): Promise; +} diff --git a/src/tree/table/TableAccountModel.ts b/src/tree/docdb/models/DocumentDBAccountModel.ts similarity index 73% rename from src/tree/table/TableAccountModel.ts rename to src/tree/docdb/models/DocumentDBAccountModel.ts index b4d8bd397..4a1e9eda8 100644 --- a/src/tree/table/TableAccountModel.ts +++ b/src/tree/docdb/models/DocumentDBAccountModel.ts @@ -3,6 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type CosmosAccountModel } from '../CosmosAccountModel'; +import { type CosmosAccountModel } from '../../CosmosAccountModel'; -export type TableAccountModel = CosmosAccountModel; +export type DocumentDBAccountModel = CosmosAccountModel; diff --git a/src/tree/docdb/models/DocumentDBContainerModel.ts b/src/tree/docdb/models/DocumentDBContainerModel.ts new file mode 100644 index 000000000..626801afb --- /dev/null +++ b/src/tree/docdb/models/DocumentDBContainerModel.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ContainerDefinition, type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { type AccountInfo } from '../AccountInfo'; + +export type DocumentDBContainerModel = { + accountInfo: AccountInfo; + database: DatabaseDefinition & Resource; + container: ContainerDefinition & Resource; +}; diff --git a/src/tree/docdb/models/DocumentDBDatabaseModel.ts b/src/tree/docdb/models/DocumentDBDatabaseModel.ts new file mode 100644 index 000000000..0c442c02b --- /dev/null +++ b/src/tree/docdb/models/DocumentDBDatabaseModel.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { type AccountInfo } from '../AccountInfo'; + +export type DocumentDBDatabaseModel = { + accountInfo: AccountInfo; + database: DatabaseDefinition & Resource; +}; diff --git a/src/tree/docdb/models/DocumentDBItemModel.ts b/src/tree/docdb/models/DocumentDBItemModel.ts new file mode 100644 index 000000000..cd7c4fb8e --- /dev/null +++ b/src/tree/docdb/models/DocumentDBItemModel.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ContainerDefinition, type DatabaseDefinition, type ItemDefinition, type Resource } from '@azure/cosmos'; +import { type AccountInfo } from '../AccountInfo'; + +export type DocumentDBItemModel = { + accountInfo: AccountInfo; + database: DatabaseDefinition & Resource; + container: ContainerDefinition & Resource; + item: ItemDefinition; +}; diff --git a/src/tree/docdb/models/DocumentDBItemsModel.ts b/src/tree/docdb/models/DocumentDBItemsModel.ts new file mode 100644 index 000000000..cb8150427 --- /dev/null +++ b/src/tree/docdb/models/DocumentDBItemsModel.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ContainerDefinition, type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { type AccountInfo } from '../AccountInfo'; + +export type DocumentDBItemsModel = { + accountInfo: AccountInfo; + database: DatabaseDefinition & Resource; + container: ContainerDefinition & Resource; +}; diff --git a/src/tree/docdb/models/DocumentDBStoredProcedureModel.ts b/src/tree/docdb/models/DocumentDBStoredProcedureModel.ts new file mode 100644 index 000000000..1b4e43ef2 --- /dev/null +++ b/src/tree/docdb/models/DocumentDBStoredProcedureModel.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + type ContainerDefinition, + type DatabaseDefinition, + type Resource, + type StoredProcedureDefinition, +} from '@azure/cosmos'; +import { type AccountInfo } from '../AccountInfo'; + +export type DocumentDBStoredProcedureModel = { + accountInfo: AccountInfo; + database: DatabaseDefinition & Resource; + container: ContainerDefinition & Resource; + procedure: StoredProcedureDefinition & Resource; +}; diff --git a/src/tree/docdb/models/DocumentDBStoredProceduresModel.ts b/src/tree/docdb/models/DocumentDBStoredProceduresModel.ts new file mode 100644 index 000000000..0b541fac3 --- /dev/null +++ b/src/tree/docdb/models/DocumentDBStoredProceduresModel.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ContainerDefinition, type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { type AccountInfo } from '../AccountInfo'; + +export type DocumentDBStoredProceduresModel = { + accountInfo: AccountInfo; + database: DatabaseDefinition & Resource; + container: ContainerDefinition & Resource; +}; diff --git a/src/tree/docdb/models/DocumentDBTriggerModel.ts b/src/tree/docdb/models/DocumentDBTriggerModel.ts new file mode 100644 index 000000000..a555c9fec --- /dev/null +++ b/src/tree/docdb/models/DocumentDBTriggerModel.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + type ContainerDefinition, + type DatabaseDefinition, + type Resource, + type TriggerDefinition, +} from '@azure/cosmos'; +import { type AccountInfo } from '../AccountInfo'; + +export type DocumentDBTriggerModel = { + accountInfo: AccountInfo; + database: DatabaseDefinition & Resource; + container: ContainerDefinition & Resource; + trigger: TriggerDefinition & Resource; +}; diff --git a/src/tree/docdb/models/DocumentDBTriggersModel.ts b/src/tree/docdb/models/DocumentDBTriggersModel.ts new file mode 100644 index 000000000..7e42fb09b --- /dev/null +++ b/src/tree/docdb/models/DocumentDBTriggersModel.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ContainerDefinition, type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { type AccountInfo } from '../AccountInfo'; + +export type DocumentDBTriggersModel = { + accountInfo: AccountInfo; + database: DatabaseDefinition & Resource; + container: ContainerDefinition & Resource; +}; diff --git a/src/tree/graph/GraphAccountAttachedResourceItem.ts b/src/tree/graph/GraphAccountAttachedResourceItem.ts new file mode 100644 index 000000000..17237bd0a --- /dev/null +++ b/src/tree/graph/GraphAccountAttachedResourceItem.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBAttachedAccountModel } from '../attached/CosmosDBAttachedAccountModel'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type AccountInfo } from '../docdb/AccountInfo'; +import { DocumentDBAccountAttachedResourceItem } from '../docdb/DocumentDBAccountAttachedResourceItem'; +import { GraphDatabaseResourceItem } from './GraphDatabaseResourceItem'; + +export class GraphAccountAttachedResourceItem extends DocumentDBAccountAttachedResourceItem { + constructor(account: CosmosDBAttachedAccountModel, experience: Experience) { + super(account, experience); + } + + protected getChildrenImpl( + accountInfo: AccountInfo, + databases: (DatabaseDefinition & Resource)[], + ): Promise { + return Promise.resolve( + databases.map((db) => { + return new GraphDatabaseResourceItem( + { + accountInfo: accountInfo, + database: db, + }, + this.experience, + ); + }), + ); + } +} diff --git a/src/tree/graph/GraphAccountResourceItem.ts b/src/tree/graph/GraphAccountResourceItem.ts index 484c31627..a985015c8 100644 --- a/src/tree/graph/GraphAccountResourceItem.ts +++ b/src/tree/graph/GraphAccountResourceItem.ts @@ -3,27 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { tryGetGremlinEndpointFromAzure } from '../../graph/gremlinEndpoints'; -import { nonNullProp } from '../../utils/nonNull'; -import { type IGremlinEndpoint } from '../../vscode-cosmosdbgraph.api'; -import { DocumentDBAccountResourceItem } from '../DocumentDBAccountResourceItem'; +import { type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type AccountInfo } from '../docdb/AccountInfo'; +import { DocumentDBAccountResourceItem } from '../docdb/DocumentDBAccountResourceItem'; +import { type DocumentDBAccountModel } from '../docdb/models/DocumentDBAccountModel'; +import { GraphDatabaseResourceItem } from './GraphDatabaseResourceItem'; export class GraphAccountResourceItem extends DocumentDBAccountResourceItem { - public gremlinEndpoint?: IGremlinEndpoint; - - protected override async init(): Promise { - await super.init(); - - const name = nonNullProp(this.account, 'name'); - const resourceGroup = nonNullProp(this.account, 'resourceGroup'); - const client = await this.getClient(); - - if (!client) { - return; - } - - this.gremlinEndpoint = await tryGetGremlinEndpointFromAzure(client, resourceGroup, name); + constructor(account: DocumentDBAccountModel, experience: Experience) { + super(account, experience); } - // here, we can add more methods or properties specific to MongoDB + protected getChildrenImpl( + accountInfo: AccountInfo, + databases: (DatabaseDefinition & Resource)[], + ): Promise { + return Promise.resolve( + databases.map((db) => { + return new GraphDatabaseResourceItem( + { + accountInfo: accountInfo, + database: db, + }, + this.experience, + ); + }), + ); + } } diff --git a/src/tree/graph/GraphContainerResourceItem.ts b/src/tree/graph/GraphContainerResourceItem.ts new file mode 100644 index 000000000..3f225aaf8 --- /dev/null +++ b/src/tree/graph/GraphContainerResourceItem.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { DocumentDBContainerResourceItem } from '../docdb/DocumentDBContainerResourceItem'; +import { type DocumentDBContainerModel } from '../docdb/models/DocumentDBContainerModel'; +import { GraphItemsResourceItem } from './GraphItemsResourceItem'; +import { GraphStoredProceduresResourceItem } from './GraphStoredProceduresResourceItem'; + +export class GraphContainerResourceItem extends DocumentDBContainerResourceItem { + constructor(model: DocumentDBContainerModel, experience: Experience) { + super(model, experience); + } + + protected getChildrenTriggersImpl(): Promise { + return Promise.resolve(undefined); + } + + protected getChildrenStoredProceduresImpl(): Promise { + return Promise.resolve(new GraphStoredProceduresResourceItem({ ...this.model }, this.experience)); + } + + protected getChildrenItemsImpl(): Promise { + return Promise.resolve(new GraphItemsResourceItem({ ...this.model }, this.experience)); + } +} diff --git a/src/tree/graph/GraphDatabaseResourceItem.ts b/src/tree/graph/GraphDatabaseResourceItem.ts new file mode 100644 index 000000000..fa0b658f0 --- /dev/null +++ b/src/tree/graph/GraphDatabaseResourceItem.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ContainerDefinition, type Resource } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { DocumentDBDatabaseResourceItem } from '../docdb/DocumentDBDatabaseResourceItem'; +import { type DocumentDBDatabaseModel } from '../docdb/models/DocumentDBDatabaseModel'; +import { GraphContainerResourceItem } from './GraphContainerResourceItem'; + +export class GraphDatabaseResourceItem extends DocumentDBDatabaseResourceItem { + constructor(model: DocumentDBDatabaseModel, experience: Experience) { + super(model, experience); + } + + protected getChildrenImpl(containers: (ContainerDefinition & Resource)[]): Promise { + return Promise.resolve( + containers.map( + (container) => new GraphContainerResourceItem({ ...this.model, container }, this.experience), + ), + ); + } +} diff --git a/src/tree/graph/GraphItemResourceItem.ts b/src/tree/graph/GraphItemResourceItem.ts new file mode 100644 index 000000000..b62cd42b4 --- /dev/null +++ b/src/tree/graph/GraphItemResourceItem.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Experience } from '../../AzureDBExperiences'; +import { DocumentDBItemResourceItem } from '../docdb/DocumentDBItemResourceItem'; +import { type DocumentDBItemModel } from '../docdb/models/DocumentDBItemModel'; + +export class GraphItemResourceItem extends DocumentDBItemResourceItem { + constructor(model: DocumentDBItemModel, experience: Experience) { + super(model, experience); + } +} diff --git a/src/tree/graph/GraphItemsResourceItem.ts b/src/tree/graph/GraphItemsResourceItem.ts new file mode 100644 index 000000000..691474f49 --- /dev/null +++ b/src/tree/graph/GraphItemsResourceItem.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ItemDefinition } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { DocumentDBItemsResourceItem } from '../docdb/DocumentDBItemsResourceItem'; +import { type DocumentDBItemsModel } from '../docdb/models/DocumentDBItemsModel'; +import { GraphItemResourceItem } from './GraphItemResourceItem'; + +export class GraphItemsResourceItem extends DocumentDBItemsResourceItem { + constructor(model: DocumentDBItemsModel, experience: Experience) { + super(model, experience); + } + + protected getChildrenImpl(items: ItemDefinition[]): Promise { + return Promise.resolve( + items.map((item) => new GraphItemResourceItem({ ...this.model, item }, this.experience)), + ); + } +} diff --git a/src/tree/graph/GraphStoredProcedureResourceItem.ts b/src/tree/graph/GraphStoredProcedureResourceItem.ts new file mode 100644 index 000000000..0be71766e --- /dev/null +++ b/src/tree/graph/GraphStoredProcedureResourceItem.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Experience } from '../../AzureDBExperiences'; +import { DocumentDBStoredProcedureResourceItem } from '../docdb/DocumentDBStoredProcedureResourceItem'; +import { type DocumentDBStoredProcedureModel } from '../docdb/models/DocumentDBStoredProcedureModel'; + +export class GraphStoredProcedureResourceItem extends DocumentDBStoredProcedureResourceItem { + constructor(model: DocumentDBStoredProcedureModel, experience: Experience) { + super(model, experience); + } +} diff --git a/src/tree/graph/GraphStoredProceduresResourceItem.ts b/src/tree/graph/GraphStoredProceduresResourceItem.ts new file mode 100644 index 000000000..a98d522d2 --- /dev/null +++ b/src/tree/graph/GraphStoredProceduresResourceItem.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Resource, type StoredProcedureDefinition } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { DocumentDBStoredProceduresResourceItem } from '../docdb/DocumentDBStoredProceduresResourceItem'; +import { type DocumentDBStoredProceduresModel } from '../docdb/models/DocumentDBStoredProceduresModel'; +import { GraphStoredProcedureResourceItem } from './GraphStoredProcedureResourceItem'; + +export class GraphStoredProceduresResourceItem extends DocumentDBStoredProceduresResourceItem { + constructor(model: DocumentDBStoredProceduresModel, experience: Experience) { + super(model, experience); + } + + protected getChildrenImpl( + storedProcedures: (StoredProcedureDefinition & Resource)[], + ): Promise { + return Promise.resolve( + storedProcedures.map( + (procedure) => new GraphStoredProcedureResourceItem({ ...this.model, procedure }, this.experience), + ), + ); + } +} diff --git a/src/tree/mongo/DatabaseItem.ts b/src/tree/mongo/DatabaseItem.ts index 5a443ae0d..76ea0b512 100644 --- a/src/tree/mongo/DatabaseItem.ts +++ b/src/tree/mongo/DatabaseItem.ts @@ -5,11 +5,11 @@ import { createGenericElement } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type CosmosDbTreeElement } from '../CosmosDbTreeElement'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; import { type IDatabaseInfo } from './IDatabaseInfo'; import { type MongoAccountModel } from './MongoAccountModel'; -export class DatabaseItem implements CosmosDbTreeElement { +export class DatabaseItem implements CosmosDBTreeElement { id: string; constructor( @@ -19,7 +19,7 @@ export class DatabaseItem implements CosmosDbTreeElement { this.id = `${account.id}/${databaseInfo.name}`; } - async getChildren(): Promise { + async getChildren(): Promise { return [ createGenericElement({ contextValue: 'mongoClusters.item.no-collection', @@ -27,7 +27,7 @@ export class DatabaseItem implements CosmosDbTreeElement { label: 'Create collection...', commandId: 'command.mongoClusters.createCollection', commandArgs: [this], - }) as CosmosDbTreeElement, + }) as CosmosDBTreeElement, ]; } // const client: MongoClustersClient = await MongoClustersClient.getClient(this.mongoCluster.id); diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index 2b1d547f0..6b6afe7f0 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -19,7 +19,7 @@ import { connectToMongoClient } from '../../mongo/connectToMongoClient'; import { getDatabaseNameFromConnectionString } from '../../mongo/mongoConnectionStrings'; import { createCosmosDBManagementClient } from '../../utils/azureClients'; import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; -import { type CosmosDbTreeElement } from '../CosmosDbTreeElement'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; import { DatabaseItem } from './DatabaseItem'; import { type IDatabaseInfo } from './IDatabaseInfo'; import { type MongoAccountModel } from './MongoAccountModel'; @@ -73,7 +73,7 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { return result ?? undefined; } - async getChildren(): Promise { + async getChildren(): Promise { ext.outputChannel.appendLine(`Cosmos DB for MongoDB (RU): Loading details for "${this.account.name}"`); let mongoClient: MongoClient | undefined; diff --git a/src/tree/nosql/NoSqlAccountAttachedResourceItem.ts b/src/tree/nosql/NoSqlAccountAttachedResourceItem.ts new file mode 100644 index 000000000..749561ef5 --- /dev/null +++ b/src/tree/nosql/NoSqlAccountAttachedResourceItem.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBAttachedAccountModel } from '../attached/CosmosDBAttachedAccountModel'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type AccountInfo } from '../docdb/AccountInfo'; +import { DocumentDBAccountAttachedResourceItem } from '../docdb/DocumentDBAccountAttachedResourceItem'; +import { NoSqlDatabaseResourceItem } from './NoSqlDatabaseResourceItem'; + +export class NoSqlAccountAttachedResourceItem extends DocumentDBAccountAttachedResourceItem { + constructor(account: CosmosDBAttachedAccountModel, experience: Experience) { + super(account, experience); + } + + protected getChildrenImpl( + accountInfo: AccountInfo, + databases: (DatabaseDefinition & Resource)[], + ): Promise { + return Promise.resolve( + databases.map((db) => { + return new NoSqlDatabaseResourceItem( + { + accountInfo: accountInfo, + database: db, + }, + this.experience, + ); + }), + ); + } +} diff --git a/src/tree/nosql/NoSqlAccountResourceItem.ts b/src/tree/nosql/NoSqlAccountResourceItem.ts index 00c4ea96e..8860dd0c0 100644 --- a/src/tree/nosql/NoSqlAccountResourceItem.ts +++ b/src/tree/nosql/NoSqlAccountResourceItem.ts @@ -3,6 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DocumentDBAccountResourceItem } from '../DocumentDBAccountResourceItem'; +import { type DatabaseDefinition, type Resource } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type AccountInfo } from '../docdb/AccountInfo'; +import { DocumentDBAccountResourceItem } from '../docdb/DocumentDBAccountResourceItem'; +import { type DocumentDBAccountModel } from '../docdb/models/DocumentDBAccountModel'; +import { NoSqlDatabaseResourceItem } from './NoSqlDatabaseResourceItem'; -export class NoSqlAccountResourceItem extends DocumentDBAccountResourceItem {} +export class NoSqlAccountResourceItem extends DocumentDBAccountResourceItem { + constructor(account: DocumentDBAccountModel, experience: Experience) { + super(account, experience); + } + + protected getChildrenImpl( + accountInfo: AccountInfo, + databases: (DatabaseDefinition & Resource)[], + ): Promise { + return Promise.resolve( + databases.map((db) => { + return new NoSqlDatabaseResourceItem( + { + accountInfo: accountInfo, + database: db, + }, + this.experience, + ); + }), + ); + } +} diff --git a/src/tree/nosql/NoSqlContainerResourceItem.ts b/src/tree/nosql/NoSqlContainerResourceItem.ts new file mode 100644 index 000000000..46078a585 --- /dev/null +++ b/src/tree/nosql/NoSqlContainerResourceItem.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { DocumentDBContainerResourceItem } from '../docdb/DocumentDBContainerResourceItem'; +import { type DocumentDBContainerModel } from '../docdb/models/DocumentDBContainerModel'; +import { NoSqlItemsResourceItem } from './NoSqlItemsResourceItem'; +import { NoSqlStoredProceduresResourceItem } from './NoSqlStoredProceduresResourceItem'; +import { NoSqlTriggersResourceItem } from './NoSqlTriggersResourceItem'; + +export class NoSqlContainerResourceItem extends DocumentDBContainerResourceItem { + constructor(model: DocumentDBContainerModel, experience: Experience) { + super(model, experience); + } + + protected getChildrenTriggersImpl(): Promise { + return Promise.resolve(new NoSqlTriggersResourceItem({ ...this.model }, this.experience)); + } + + protected getChildrenStoredProceduresImpl(): Promise { + return Promise.resolve(new NoSqlStoredProceduresResourceItem({ ...this.model }, this.experience)); + } + + protected getChildrenItemsImpl(): Promise { + return Promise.resolve(new NoSqlItemsResourceItem({ ...this.model }, this.experience)); + } +} diff --git a/src/tree/nosql/NoSqlDatabaseResourceItem.ts b/src/tree/nosql/NoSqlDatabaseResourceItem.ts new file mode 100644 index 000000000..8dbe327e6 --- /dev/null +++ b/src/tree/nosql/NoSqlDatabaseResourceItem.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ContainerDefinition, type Resource } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { DocumentDBDatabaseResourceItem } from '../docdb/DocumentDBDatabaseResourceItem'; +import { type DocumentDBDatabaseModel } from '../docdb/models/DocumentDBDatabaseModel'; +import { NoSqlContainerResourceItem } from './NoSqlContainerResourceItem'; + +export class NoSqlDatabaseResourceItem extends DocumentDBDatabaseResourceItem { + constructor(model: DocumentDBDatabaseModel, experience: Experience) { + super(model, experience); + } + + protected getChildrenImpl(containers: (ContainerDefinition & Resource)[]): Promise { + return Promise.resolve( + containers.map( + (container) => new NoSqlContainerResourceItem({ ...this.model, container }, this.experience), + ), + ); + } +} diff --git a/src/tree/nosql/NoSqlItemResourceItem.ts b/src/tree/nosql/NoSqlItemResourceItem.ts new file mode 100644 index 000000000..9620a738f --- /dev/null +++ b/src/tree/nosql/NoSqlItemResourceItem.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Experience } from '../../AzureDBExperiences'; +import { DocumentDBItemResourceItem } from '../docdb/DocumentDBItemResourceItem'; +import { type DocumentDBItemModel } from '../docdb/models/DocumentDBItemModel'; + +export class NoSqlItemResourceItem extends DocumentDBItemResourceItem { + constructor(model: DocumentDBItemModel, experience: Experience) { + super(model, experience); + } +} diff --git a/src/tree/nosql/NoSqlItemsResourceItem.ts b/src/tree/nosql/NoSqlItemsResourceItem.ts new file mode 100644 index 000000000..42da5ab92 --- /dev/null +++ b/src/tree/nosql/NoSqlItemsResourceItem.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ItemDefinition } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { DocumentDBItemsResourceItem } from '../docdb/DocumentDBItemsResourceItem'; +import { type DocumentDBItemsModel } from '../docdb/models/DocumentDBItemsModel'; +import { NoSqlItemResourceItem } from './NoSqlItemResourceItem'; + +export class NoSqlItemsResourceItem extends DocumentDBItemsResourceItem { + constructor(model: DocumentDBItemsModel, experience: Experience) { + super(model, experience); + } + + protected getChildrenImpl(items: ItemDefinition[]): Promise { + return Promise.resolve( + items.map((item) => new NoSqlItemResourceItem({ ...this.model, item }, this.experience)), + ); + } +} diff --git a/src/tree/nosql/NoSqlStoredProcedureResourceItem.ts b/src/tree/nosql/NoSqlStoredProcedureResourceItem.ts new file mode 100644 index 000000000..9ef0874af --- /dev/null +++ b/src/tree/nosql/NoSqlStoredProcedureResourceItem.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Experience } from '../../AzureDBExperiences'; +import { DocumentDBStoredProcedureResourceItem } from '../docdb/DocumentDBStoredProcedureResourceItem'; +import { type DocumentDBStoredProcedureModel } from '../docdb/models/DocumentDBStoredProcedureModel'; + +export class NoSqlStoredProcedureResourceItem extends DocumentDBStoredProcedureResourceItem { + constructor(model: DocumentDBStoredProcedureModel, experience: Experience) { + super(model, experience); + } +} diff --git a/src/tree/nosql/NoSqlStoredProceduresResourceItem.ts b/src/tree/nosql/NoSqlStoredProceduresResourceItem.ts new file mode 100644 index 000000000..760926298 --- /dev/null +++ b/src/tree/nosql/NoSqlStoredProceduresResourceItem.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Resource, type StoredProcedureDefinition } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { DocumentDBStoredProceduresResourceItem } from '../docdb/DocumentDBStoredProceduresResourceItem'; +import { type DocumentDBStoredProceduresModel } from '../docdb/models/DocumentDBStoredProceduresModel'; +import { NoSqlStoredProcedureResourceItem } from './NoSqlStoredProcedureResourceItem'; + +export class NoSqlStoredProceduresResourceItem extends DocumentDBStoredProceduresResourceItem { + constructor(model: DocumentDBStoredProceduresModel, experience: Experience) { + super(model, experience); + } + + protected getChildrenImpl( + storedProcedures: (StoredProcedureDefinition & Resource)[], + ): Promise { + return Promise.resolve( + storedProcedures.map( + (procedure) => new NoSqlStoredProcedureResourceItem({ ...this.model, procedure }, this.experience), + ), + ); + } +} diff --git a/src/tree/nosql/NoSqlTriggerResourceItem.ts b/src/tree/nosql/NoSqlTriggerResourceItem.ts new file mode 100644 index 000000000..52a6f0f25 --- /dev/null +++ b/src/tree/nosql/NoSqlTriggerResourceItem.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Experience } from '../../AzureDBExperiences'; +import { DocumentDBTriggerResourceItem } from '../docdb/DocumentDBTriggerResourceItem'; +import { type DocumentDBTriggerModel } from '../docdb/models/DocumentDBTriggerModel'; + +export class NoSqlTriggerResourceItem extends DocumentDBTriggerResourceItem { + constructor(model: DocumentDBTriggerModel, experience: Experience) { + super(model, experience); + } +} diff --git a/src/tree/nosql/NoSqlTriggersResourceItem.ts b/src/tree/nosql/NoSqlTriggersResourceItem.ts new file mode 100644 index 000000000..b8569394f --- /dev/null +++ b/src/tree/nosql/NoSqlTriggersResourceItem.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type Resource, type TriggerDefinition } from '@azure/cosmos'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { DocumentDBTriggersResourceItem } from '../docdb/DocumentDBTriggersResourceItem'; +import { type DocumentDBTriggersModel } from '../docdb/models/DocumentDBTriggersModel'; +import { NoSqlTriggerResourceItem } from './NoSqlTriggerResourceItem'; + +export class NoSqlTriggersResourceItem extends DocumentDBTriggersResourceItem { + constructor(model: DocumentDBTriggersModel, experience: Experience) { + super(model, experience); + } + + protected getChildrenImpl(triggers: (TriggerDefinition & Resource)[]): Promise { + return Promise.resolve( + triggers.map((trigger) => new NoSqlTriggerResourceItem({ ...this.model, trigger }, this.experience)), + ); + } +} diff --git a/src/tree/table/TableAccountAttachedResourceItem.ts b/src/tree/table/TableAccountAttachedResourceItem.ts new file mode 100644 index 000000000..b42e68548 --- /dev/null +++ b/src/tree/table/TableAccountAttachedResourceItem.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + callWithTelemetryAndErrorHandling, + createGenericElement, + type IActionContext, +} from '@microsoft/vscode-azext-utils'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBAttachedAccountModel } from '../attached/CosmosDBAttachedAccountModel'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { DocumentDBAccountAttachedResourceItem } from '../docdb/DocumentDBAccountAttachedResourceItem'; + +export class TableAccountAttachedResourceItem extends DocumentDBAccountAttachedResourceItem { + constructor(account: CosmosDBAttachedAccountModel, experience: Experience) { + super(account, experience); + } + + public async getChildren(): Promise { + const result = await callWithTelemetryAndErrorHandling('getChildren', (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + return Promise.resolve([ + createGenericElement({ + contextValue: this.contextValue, + label: 'Table Accounts are not supported yet.', + id: `${this.id}/no-databases`, + }) as CosmosDBTreeElement, + ]); + }); + + return result ?? []; + } + + protected getChildrenImpl(): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/src/tree/table/TableAccountResourceItem.ts b/src/tree/table/TableAccountResourceItem.ts index 245123c0b..7cbba8686 100644 --- a/src/tree/table/TableAccountResourceItem.ts +++ b/src/tree/table/TableAccountResourceItem.ts @@ -3,18 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createGenericElement } from '@microsoft/vscode-azext-utils'; -import { type CosmosDbTreeElement } from '../CosmosDbTreeElement'; -import { DocumentDBAccountResourceItem } from '../DocumentDBAccountResourceItem'; +import { + callWithTelemetryAndErrorHandling, + createGenericElement, + type IActionContext, +} from '@microsoft/vscode-azext-utils'; +import { type Experience } from '../../AzureDBExperiences'; +import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { DocumentDBAccountResourceItem } from '../docdb/DocumentDBAccountResourceItem'; +import { type DocumentDBAccountModel } from '../docdb/models/DocumentDBAccountModel'; export class TableAccountResourceItem extends DocumentDBAccountResourceItem { - public getChildren(): Promise { - return Promise.resolve([ - createGenericElement({ - contextValue: 'tableNotSupported', - label: 'Table Accounts are not supported yet.', - id: `${this.id}/no-databases`, - }) as CosmosDbTreeElement, - ]); + constructor(account: DocumentDBAccountModel, experience: Experience) { + super(account, experience); + } + + public async getChildren(): Promise { + const result = await callWithTelemetryAndErrorHandling('getChildren', (context: IActionContext) => { + context.telemetry.properties.experience = this.experience.api; + context.telemetry.properties.parentContext = this.contextValue; + + return Promise.resolve([ + createGenericElement({ + contextValue: this.contextValue, + label: 'Table Accounts are not supported yet.', + id: `${this.id}/no-databases`, + }) as CosmosDBTreeElement, + ]); + }); + + return result ?? []; + } + + protected getChildrenImpl(): Promise { + throw new Error('Method not implemented.'); } } diff --git a/src/tree/workspace/sharedWorkspaceResourceProvider.ts b/src/tree/workspace/SharedWorkspaceResourceProvider.ts similarity index 90% rename from src/tree/workspace/sharedWorkspaceResourceProvider.ts rename to src/tree/workspace/SharedWorkspaceResourceProvider.ts index 83df9f50f..c7de7c34a 100644 --- a/src/tree/workspace/sharedWorkspaceResourceProvider.ts +++ b/src/tree/workspace/SharedWorkspaceResourceProvider.ts @@ -32,6 +32,7 @@ import type * as vscode from 'vscode'; */ export enum WorkspaceResourceType { MongoClusters = 'vscode.cosmosdb.workspace.mongoclusters-resourceType', + AttachedAccounts = 'vscode.cosmosdb.workspace.attachedaccounts-resourceType', } /** @@ -57,6 +58,11 @@ export class SharedWorkspaceResourceProvider implements WorkspaceResourceProvide id: 'vscode.cosmosdb.workspace.mongoclusters', name: 'MongoDB Cluster Accounts', // this name will be displayed in the workspace view, when no WorkspaceResourceBranchDataProvider is registered }, + { + resourceType: WorkspaceResourceType.AttachedAccounts, + id: 'vscode.cosmosdb.workspace.attachedaccounts', + name: 'Attached Database Accounts', + }, ]; } } diff --git a/src/tree/workspace/sharedWorkspaceStorage.ts b/src/tree/workspace/SharedWorkspaceStorage.ts similarity index 99% rename from src/tree/workspace/sharedWorkspaceStorage.ts rename to src/tree/workspace/SharedWorkspaceStorage.ts index 68f903812..800b0ecd2 100644 --- a/src/tree/workspace/sharedWorkspaceStorage.ts +++ b/src/tree/workspace/SharedWorkspaceStorage.ts @@ -25,7 +25,7 @@ export type SharedWorkspaceStorageItem = { /** * Optional properties associated with the item. */ - properties?: Record; + properties?: Record; /** * Optional array of secrets associated with the item. diff --git a/src/utils/document.ts b/src/utils/document.ts index 374127354..3b75b8994 100644 --- a/src/utils/document.ts +++ b/src/utils/document.ts @@ -22,7 +22,7 @@ export const extractPartitionKey = (document: ItemDefinition, partitionKey: Part // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment interim = interim[prop]; } else { - return null; // It is not correct to return null, in other cases it should exception + return null; // It is not correct to return null, in other cases it should be an exception } } if ( From f2fcaad6a352785bd3c5fa6dd63da74583965234 Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Thu, 9 Jan 2025 11:29:09 +0100 Subject: [PATCH 21/42] feat: Migrating TreeView to V2 --- .../DocumentDBAccountAttachedResourceItem.ts | 53 ++++++++++++++++--- .../docdb/DocumentDBAccountResourceItem.ts | 2 +- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts index 625202e1c..72f22a848 100644 --- a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts @@ -12,6 +12,7 @@ import { parseDocDBConnectionString } from '../../docdb/docDBConnectionStrings'; import { type CosmosDBCredential, type CosmosDBKeyCredential, getCosmosClient } from '../../docdb/getCosmosClient'; import { getSignedInPrincipalIdForAccountEndpoint } from '../../docdb/utils/azureSessionHelper'; import { isRbacException, showRbacPermissionError } from '../../docdb/utils/rbacUtils'; +import { localize } from '../../utils/localize'; import { type CosmosDBAttachedAccountModel } from '../attached/CosmosDBAttachedAccountModel'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; import { type AccountInfo } from './AccountInfo'; @@ -62,7 +63,7 @@ export abstract class DocumentDBAccountAttachedResourceItem implements CosmosDBT const isEmulator = account.isEmulator; const parsedCS = parseDocDBConnectionString(account.connectionString); const documentEndpoint = parsedCS.documentEndpoint; - const credentials = await this.getCredentials(account.connectionString); + const credentials = await this.getCredentials(account); return { credentials, @@ -96,7 +97,7 @@ export abstract class DocumentDBAccountAttachedResourceItem implements CosmosDBT } } - protected async getCredentials(connectionString: string): Promise { + protected async getCredentials(account: CosmosDBAttachedAccountModel): Promise { const result = await callWithTelemetryAndErrorHandling('getCredentials', async (context: IActionContext) => { context.telemetry.properties.experience = this.experience.api; context.telemetry.properties.parentContext = this.contextValue; @@ -107,11 +108,49 @@ export abstract class DocumentDBAccountAttachedResourceItem implements CosmosDBT let keyCred: CosmosDBKeyCredential | undefined = undefined; // disable key auth if the user has opted in to OAuth (AAD/Entra ID) if (!forceOAuth) { - const parsedCS = parseDocDBConnectionString(connectionString); - keyCred = { - type: 'key', - key: parsedCS.masterKey, - }; + let localAuthDisabled = false; + + const parsedCS = parseDocDBConnectionString(account.connectionString); + if (parsedCS.masterKey) { + context.telemetry.properties.receivedKeyCreds = 'true'; + + keyCred = { + type: 'key', + key: parsedCS.masterKey, + }; + + try { + // Since here we don't have subscription, + // we can't get DatabaseAccountGetResults to retrieve disableLocalAuth property + // Will try to connect to the account and if it fails, we will assume local auth is disabled + const cosmosClient = getCosmosClient(parsedCS.documentEndpoint, [keyCred], account.isEmulator); + await cosmosClient.getDatabaseAccount(); + } catch { + context.telemetry.properties.receivedKeyCreds = 'false'; + localAuthDisabled = true; + } + } + + context.telemetry.properties.localAuthDisabled = localAuthDisabled.toString(); + if (localAuthDisabled) { + // Clean up keyCred if local auth is disabled + keyCred = undefined; + + const message = localize( + 'keyPermissionErrorMsg', + 'You do not have the required permissions to list auth keys for [{0}].\nFalling back to using Entra ID.\nYou can change the default authentication in the settings.', + account.name, + ); + const openSettingsItem = localize('openSettings', 'Open Settings'); + void vscode.window.showWarningMessage(message, ...[openSettingsItem]).then((item) => { + if (item === openSettingsItem) { + void vscode.commands.executeCommand( + 'workbench.action.openSettings', + 'azureDatabases.useCosmosOAuth', + ); + } + }); + } } // OAuth is always enabled for Cosmos DB and will be used as a fallback if key auth is unavailable diff --git a/src/tree/docdb/DocumentDBAccountResourceItem.ts b/src/tree/docdb/DocumentDBAccountResourceItem.ts index 5a5bf8418..cf89a5ef6 100644 --- a/src/tree/docdb/DocumentDBAccountResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountResourceItem.ts @@ -119,7 +119,6 @@ export abstract class DocumentDBAccountResourceItem implements CosmosDBTreeEleme context.telemetry.properties.experience = this.experience.api; context.telemetry.properties.parentContext = this.contextValue; - const localAuthDisabled = databaseAccount.disableLocalAuth === true; const forceOAuth = vscode.workspace.getConfiguration().get('azureDatabases.useCosmosOAuth'); context.telemetry.properties.useCosmosOAuth = (forceOAuth ?? false).toString(); @@ -127,6 +126,7 @@ export abstract class DocumentDBAccountResourceItem implements CosmosDBTreeEleme // disable key auth if the user has opted in to OAuth (AAD/Entra ID) if (!forceOAuth) { try { + const localAuthDisabled = databaseAccount.disableLocalAuth === true; context.telemetry.properties.localAuthDisabled = localAuthDisabled.toString(); let keyResult: DatabaseAccountListKeysResult | undefined; From 54b7ed8cfa28b15db6b2d1204eb71d2feb3e1fc1 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 9 Jan 2025 13:20:48 +0100 Subject: [PATCH 22/42] added command 'copy connection string' --- package.json | 10 ++++ src/mongoClusters/MongoClustersExtension.ts | 5 ++ .../commands/copyConnectionString.ts | 49 +++++++++++++++++++ .../tree/MongoClusterItemBase.ts | 8 +++ .../tree/MongoClusterResourceItem.ts | 33 +++++++++++++ .../workspace/MongoClusterWorkspaceItem.ts | 5 ++ src/tree/mongo/MongoAccountResourceItem.ts | 6 +-- 7 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 src/mongoClusters/commands/copyConnectionString.ts diff --git a/package.json b/package.json index 96fb209db..db366c8da 100644 --- a/package.json +++ b/package.json @@ -584,6 +584,11 @@ "command": "command.mongoClusters.importDocuments", "title": "Import Documents into Collection..." }, + { + "category": "MongoDB Clusters", + "command": "command.mongoClusters.copyConnectionString", + "title": "Copy Connection String" + }, { "category": "MongoDB Clusters", "command": "command.mongoClusters.exportDocuments", @@ -1092,6 +1097,11 @@ "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.mongoCluster/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "1@1" }, + { + "command": "command.mongoClusters.copyConnectionString", + "when": "viewItem =~ /mongodb.item.account/i || viewItem =~ /treeitem.mongoCluster/i && viewItem =~ /(mongocluster)/i", + "group": "1@1" + }, { "command": "command.mongoClusters.importDocuments", "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", diff --git a/src/mongoClusters/MongoClustersExtension.ts b/src/mongoClusters/MongoClustersExtension.ts index 0959316a2..1fe6eaaa3 100644 --- a/src/mongoClusters/MongoClustersExtension.ts +++ b/src/mongoClusters/MongoClustersExtension.ts @@ -23,6 +23,7 @@ import { WorkspaceResourceType, } from '../tree/workspace/sharedWorkspaceResourceProvider'; import { addWorkspaceConnection } from './commands/addWorkspaceConnection'; +import { copyConnectionString } from './commands/copyConnectionString'; import { createCollection } from './commands/createCollection'; import { createDatabase } from './commands/createDatabase'; import { createDocument } from './commands/createDocument'; @@ -88,6 +89,10 @@ export class MongoClustersExtension implements vscode.Disposable { registerCommand('command.internal.mongoClusters.documentView.open', openDocumentView); registerCommandWithTreeNodeUnwrapping('command.mongoClusters.launchShell', launchShell); + registerCommandWithTreeNodeUnwrapping( + 'command.mongoClusters.copyConnectionString', + copyConnectionString, + ); registerCommandWithTreeNodeUnwrapping('command.mongoClusters.dropCollection', dropCollection); registerCommandWithTreeNodeUnwrapping('command.mongoClusters.dropDatabase', dropDatabase); diff --git a/src/mongoClusters/commands/copyConnectionString.ts b/src/mongoClusters/commands/copyConnectionString.ts new file mode 100644 index 000000000..da9985e68 --- /dev/null +++ b/src/mongoClusters/commands/copyConnectionString.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { ext } from '../../extensionVariables'; +import { MongoAccountResourceItem } from '../../tree/mongo/MongoAccountResourceItem'; +import { localize } from '../../utils/localize'; +import { MongoClusterItemBase } from '../tree/MongoClusterItemBase'; + +export async function copyConnectionString( + context: IActionContext, + clusterNode?: MongoClusterItemBase | MongoAccountResourceItem, +): Promise { + // node ??= ... pick a node if not provided + if (!clusterNode) { + throw new Error('No cluster selected.'); + } + + const connectionString = await ext.state.runWithTemporaryDescription(clusterNode.id, 'Working...', async () => { + if (clusterNode instanceof MongoAccountResourceItem) { + context.telemetry.properties.experience = clusterNode.experience.api; + return clusterNode.discoverConnectionString(); + } + + if (clusterNode instanceof MongoClusterItemBase) { + context.telemetry.properties.experience = clusterNode.mongoCluster.dbExperience?.api; + return clusterNode.discoverConnectionString(); + } + + return undefined; + }); + + if (!connectionString) { + void vscode.window.showErrorMessage( + localize( + 'copyConnectionString.noConnectionString', + 'Failed to extract the connection string from the selected cluster.', + ), + ); + } else { + await vscode.env.clipboard.writeText(connectionString); + void vscode.window.showInformationMessage( + localize('copyConnectionString.success', 'The connection string has been copied to the clipboard'), + ); + } +} diff --git a/src/mongoClusters/tree/MongoClusterItemBase.ts b/src/mongoClusters/tree/MongoClusterItemBase.ts index 167948cfb..6a644d58f 100644 --- a/src/mongoClusters/tree/MongoClusterItemBase.ts +++ b/src/mongoClusters/tree/MongoClusterItemBase.ts @@ -42,6 +42,14 @@ export abstract class MongoClusterItemBase implements TreeElementWithId, TreeEle */ protected abstract authenticateAndConnect(): Promise; + /** + * Abstract method to get the connection string for the MongoDB cluster. + * Must be implemented by subclasses. + * + * @returns A promise that resolves to the connection string if successful; otherwise, undefined. + */ + public abstract discoverConnectionString(): Promise; + /** * Authenticates and connects to the cluster to list all available databases. * Here, the MongoDB client is created and cached for future use. diff --git a/src/mongoClusters/tree/MongoClusterResourceItem.ts b/src/mongoClusters/tree/MongoClusterResourceItem.ts index eeb7c6b4b..1199a288e 100644 --- a/src/mongoClusters/tree/MongoClusterResourceItem.ts +++ b/src/mongoClusters/tree/MongoClusterResourceItem.ts @@ -25,6 +25,8 @@ import { ProvideUserNameStep } from '../wizards/authenticate/ProvideUsernameStep import { MongoClusterItemBase } from './MongoClusterItemBase'; import { type MongoClusterModel } from './MongoClusterModel'; +import ConnectionString from 'mongodb-connection-string-url'; + export class MongoClusterResourceItem extends MongoClusterItemBase { constructor( private readonly subscription: AzureSubscription, @@ -33,6 +35,37 @@ export class MongoClusterResourceItem extends MongoClusterItemBase { super(mongoCluster); } + public async discoverConnectionString(): Promise { + return callWithTelemetryAndErrorHandling( + 'cosmosDB.mongoClusters.discoverConnectionString', + async (context: IActionContext) => { + // Create a client to interact with the MongoDB vCore management API and read the cluster details + const managementClient = await createMongoClustersManagementClient(context, this.subscription); + + const clusterInformation = await managementClient.mongoClusters.get( + this.mongoCluster.resourceGroup as string, + this.mongoCluster.name, + ); + + if (!clusterInformation.connectionString) { + return undefined; + } + + context.valuesToMask.push(clusterInformation.connectionString); + const connectionString = new ConnectionString(clusterInformation.connectionString as string); + + if (clusterInformation.administratorLogin) { + context.valuesToMask.push(clusterInformation.administratorLogin); + connectionString.username = clusterInformation.administratorLogin; + } + + connectionString.password = ''; + + return connectionString.toString(); + }, + ); + } + /** * Authenticates and connects to the MongoDB cluster. * @param context The action context. diff --git a/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts index 358a64441..a0287d507 100644 --- a/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts @@ -31,6 +31,11 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase { super(mongoCluster); } + // eslint-disable-next-line @typescript-eslint/require-await + public async discoverConnectionString(): Promise { + return this.mongoCluster.connectionString; + } + /** * Authenticates and connects to the MongoDB cluster. * @param context The action context. diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index b5c3d2eed..f854f1916 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -31,14 +31,14 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { constructor( account: MongoAccountModel, - protected experience: Experience, + readonly experience: Experience, readonly databaseAccount?: DatabaseAccountGetResults, // TODO: exploring during v1->v2 migration readonly isEmulator?: boolean, // TODO: exploring during v1->v2 migration ) { super(account); } - async discoverConnectionStringFromAccountInfo(): Promise { + async discoverConnectionString(): Promise { const result = await callWithTelemetryAndErrorHandling( 'cosmosDB.mongo.discoverConnectionString', async (context: IActionContext) => { @@ -88,7 +88,7 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { ); if (this.account.subscription) { - const cString = await this.discoverConnectionStringFromAccountInfo(); + const cString = await this.discoverConnectionString(); this.account.connectionString = cString; } From 198c70eb97fc1e3cfa99eb7c59cd723c9cfc1ecd Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Thu, 9 Jan 2025 14:45:07 +0100 Subject: [PATCH 23/42] feat: Migrating TreeView to V2 --- src/tree/CosmosDBBranchDataProvider.ts | 49 ++++++++++++------- .../CosmosDBWorkspaceBranchDataProvider.ts | 44 +++++++++++------ .../DocumentDBAccountAttachedResourceItem.ts | 1 + .../docdb/DocumentDBAccountResourceItem.ts | 1 + .../docdb/DocumentDBContainerResourceItem.ts | 1 + .../docdb/DocumentDBDatabaseResourceItem.ts | 1 + src/tree/docdb/DocumentDBItemsResourceItem.ts | 1 + .../DocumentDBStoredProceduresResourceItem.ts | 1 + .../docdb/DocumentDBTriggersResourceItem.ts | 1 + 9 files changed, 67 insertions(+), 33 deletions(-) diff --git a/src/tree/CosmosDBBranchDataProvider.ts b/src/tree/CosmosDBBranchDataProvider.ts index a40bb489b..20e298a7c 100644 --- a/src/tree/CosmosDBBranchDataProvider.ts +++ b/src/tree/CosmosDBBranchDataProvider.ts @@ -3,12 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { + callWithTelemetryAndErrorHandling, + createGenericElement, + parseError, + type IActionContext, +} from '@microsoft/vscode-azext-utils'; import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; import { API, tryGetExperience } from '../AzureDBExperiences'; import { databaseAccountType } from '../constants'; import { ext } from '../extensionVariables'; +import { localize } from '../utils/localize'; import { nonNullProp } from '../utils/nonNull'; import { type CosmosAccountModel, type CosmosDBResource } from './CosmosAccountModel'; import { type CosmosDBTreeElement } from './CosmosDBTreeElement'; @@ -36,22 +42,31 @@ export class CosmosDBBranchDataProvider * This function is called for every element in the tree when expanding, the element being expanded is being passed as an argument */ async getChildren(element: CosmosDBTreeElement): Promise { - const result = await callWithTelemetryAndErrorHandling( - 'CosmosDBBranchDataProvider.getChildren', - async (context: IActionContext) => { - const elementTreeItem = await element.getTreeItem(); - - context.telemetry.properties.parentContext = elementTreeItem.contextValue ?? 'unknown'; - - return (await element.getChildren?.())?.map((child) => { - return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => - this.refresh(child), - ) as CosmosDBTreeElement; - }); - }, - ); - - return result ?? []; + try { + const result = await callWithTelemetryAndErrorHandling( + 'CosmosDBBranchDataProvider.getChildren', + async (context: IActionContext) => { + context.errorHandling.suppressDisplay = true; + context.errorHandling.rethrow = true; + context.errorHandling.forceIncludeInReportIssueCommand = true; + + return (await element.getChildren?.())?.map((child) => { + return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => + this.refresh(child), + ) as CosmosDBTreeElement; + }); + }, + ); + + return result ?? []; + } catch (error) { + return [ + createGenericElement({ + contextValue: 'cosmosDB.item.error', + label: localize('Error: {0}', parseError(error).message), + }) as CosmosDBTreeElement, + ]; + } } /** diff --git a/src/tree/CosmosDBWorkspaceBranchDataProvider.ts b/src/tree/CosmosDBWorkspaceBranchDataProvider.ts index 6826ac2ed..5fe67fe8c 100644 --- a/src/tree/CosmosDBWorkspaceBranchDataProvider.ts +++ b/src/tree/CosmosDBWorkspaceBranchDataProvider.ts @@ -3,10 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { + callWithTelemetryAndErrorHandling, + createGenericElement, + type IActionContext, + parseError, +} from '@microsoft/vscode-azext-utils'; import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; import { ext } from '../extensionVariables'; +import { localize } from '../utils/localize'; import { type CosmosDBResource } from './CosmosAccountModel'; import { type CosmosDBTreeElement } from './CosmosDBTreeElement'; import { CosmosDBAttachedAccountsResourceItem } from './attached/CosmosDBAttachedAccountsResourceItem'; @@ -29,23 +35,29 @@ export class CosmosDBWorkspaceBranchDataProvider * This function is called for every element in the tree when expanding, the element being expanded is being passed as an argument */ async getChildren(element: CosmosDBTreeElement): Promise { - const result = await callWithTelemetryAndErrorHandling( - 'CosmosDBWorkspaceBranchDataProvider.getChildren', - async (context: IActionContext) => { - const elementTreeItem = await element.getTreeItem(); + try { + const result = await callWithTelemetryAndErrorHandling( + 'CosmosDBWorkspaceBranchDataProvider.getChildren', + async (context: IActionContext) => { + context.telemetry.properties.view = 'workspace'; - context.telemetry.properties.view = 'workspace'; - context.telemetry.properties.parentContext = elementTreeItem.contextValue ?? 'unknown'; + return (await element.getChildren?.())?.map((child) => { + return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => + this.refresh(child), + ) as CosmosDBTreeElement; + }); + }, + ); - return (await element.getChildren?.())?.map((child) => { - return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => - this.refresh(child), - ) as CosmosDBTreeElement; - }); - }, - ); - - return result ?? []; + return result ?? []; + } catch (error) { + return [ + createGenericElement({ + contextValue: 'cosmosDB.workspace.item.error', + label: localize('Error: {0}', parseError(error).message), + }) as CosmosDBTreeElement, + ]; + } } /** diff --git a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts index 72f22a848..3a62d66b0 100644 --- a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts @@ -35,6 +35,7 @@ export abstract class DocumentDBAccountAttachedResourceItem implements CosmosDBT const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { context.telemetry.properties.experience = this.experience.api; context.telemetry.properties.parentContext = this.contextValue; + context.errorHandling.rethrow = true; const accountInfo = await this.getAccountInfo(this.account); const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); diff --git a/src/tree/docdb/DocumentDBAccountResourceItem.ts b/src/tree/docdb/DocumentDBAccountResourceItem.ts index cf89a5ef6..2cd03a0cf 100644 --- a/src/tree/docdb/DocumentDBAccountResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountResourceItem.ts @@ -37,6 +37,7 @@ export abstract class DocumentDBAccountResourceItem implements CosmosDBTreeEleme const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { context.telemetry.properties.experience = this.experience.api; context.telemetry.properties.parentContext = this.contextValue; + context.errorHandling.rethrow = true; const accountInfo = await this.getAccountInfo(context, this.account); const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); diff --git a/src/tree/docdb/DocumentDBContainerResourceItem.ts b/src/tree/docdb/DocumentDBContainerResourceItem.ts index b3c141653..da22b28a5 100644 --- a/src/tree/docdb/DocumentDBContainerResourceItem.ts +++ b/src/tree/docdb/DocumentDBContainerResourceItem.ts @@ -26,6 +26,7 @@ export abstract class DocumentDBContainerResourceItem implements CosmosDBTreeEle const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { context.telemetry.properties.experience = this.experience.api; context.telemetry.properties.parentContext = this.contextValue; + context.errorHandling.rethrow = true; const triggers = await this.getChildrenTriggersImpl(); const storedProcedures = await this.getChildrenStoredProceduresImpl(); diff --git a/src/tree/docdb/DocumentDBDatabaseResourceItem.ts b/src/tree/docdb/DocumentDBDatabaseResourceItem.ts index 715273ec9..20f7e561a 100644 --- a/src/tree/docdb/DocumentDBDatabaseResourceItem.ts +++ b/src/tree/docdb/DocumentDBDatabaseResourceItem.ts @@ -28,6 +28,7 @@ export abstract class DocumentDBDatabaseResourceItem implements CosmosDBTreeElem const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { context.telemetry.properties.experience = this.experience.api; context.telemetry.properties.parentContext = this.contextValue; + context.errorHandling.rethrow = true; const { endpoint, credentials, isEmulator } = this.model.accountInfo; const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); diff --git a/src/tree/docdb/DocumentDBItemsResourceItem.ts b/src/tree/docdb/DocumentDBItemsResourceItem.ts index b33800f0f..89e085c9f 100644 --- a/src/tree/docdb/DocumentDBItemsResourceItem.ts +++ b/src/tree/docdb/DocumentDBItemsResourceItem.ts @@ -37,6 +37,7 @@ export abstract class DocumentDBItemsResourceItem implements CosmosDBTreeElement const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { context.telemetry.properties.experience = this.experience.api; context.telemetry.properties.parentContext = this.contextValue; + context.errorHandling.rethrow = true; if (this.iterator && this.cachedItems.length > 0) { // ignore diff --git a/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts b/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts index 3f49522cd..313494a47 100644 --- a/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts +++ b/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts @@ -28,6 +28,7 @@ export abstract class DocumentDBStoredProceduresResourceItem implements CosmosDB const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { context.telemetry.properties.experience = this.experience.api; context.telemetry.properties.parentContext = this.contextValue; + context.errorHandling.rethrow = true; const { endpoint, credentials, isEmulator } = this.model.accountInfo; const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); diff --git a/src/tree/docdb/DocumentDBTriggersResourceItem.ts b/src/tree/docdb/DocumentDBTriggersResourceItem.ts index fd246f3bf..3adb78907 100644 --- a/src/tree/docdb/DocumentDBTriggersResourceItem.ts +++ b/src/tree/docdb/DocumentDBTriggersResourceItem.ts @@ -28,6 +28,7 @@ export abstract class DocumentDBTriggersResourceItem implements CosmosDBTreeElem const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { context.telemetry.properties.experience = this.experience.api; context.telemetry.properties.parentContext = this.contextValue; + context.errorHandling.rethrow = true; const { endpoint, credentials, isEmulator } = this.model.accountInfo; const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); From 894037581f534c330ac588608d732d650b7b93b9 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 9 Jan 2025 15:45:05 +0100 Subject: [PATCH 24/42] resolved merge conflicts --- src/extension.ts | 2 +- .../workspace/MongoDBAccountsWorkspaceItem.ts | 6 +- src/tree/CosmosAccountResourceItemBase.ts | 1 - src/tree/CosmosDBBranchDataProvider.ts | 6 +- src/tree/mongo/DatabaseItem.ts | 63 ------------------- src/tree/mongo/MongoAccountResourceItem.ts | 15 ++--- 6 files changed, 9 insertions(+), 84 deletions(-) delete mode 100644 src/tree/mongo/DatabaseItem.ts diff --git a/src/extension.ts b/src/extension.ts index 154915136..12225d843 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -61,11 +61,11 @@ import { AttachedAccountSuffix } from './tree/AttachedAccountsTreeItem'; import { CosmosDBBranchDataProvider } from './tree/CosmosDBBranchDataProvider'; import { CosmosDBWorkspaceBranchDataProvider } from './tree/CosmosDBWorkspaceBranchDataProvider'; import { SubscriptionTreeItem } from './tree/SubscriptionTreeItem'; +import { isTreeElementWithExperience } from './tree/TreeElementWithExperience'; import { SharedWorkspaceResourceProvider, WorkspaceResourceType, } from './tree/workspace/SharedWorkspaceResourceProvider'; -import { isTreeElementWithExperience } from './tree/TreeElementWithExperience'; import { localize } from './utils/localize'; const cosmosDBTopLevelContextValues: string[] = [ diff --git a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts index 624f383e3..8e10d2cb1 100644 --- a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts @@ -5,12 +5,10 @@ import { createGenericElement, type TreeElementBase, type TreeElementWithId } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { WorkspaceResourceType } from '../../../tree/workspace/SharedWorkspaceResourceProvider'; -import { SharedWorkspaceStorage } from '../../../tree/workspace/SharedWorkspaceStorage'; import { MongoClustersExprience, type Experience } from '../../../AzureDBExperiences'; import { type TreeElementWithExperience } from '../../../tree/TreeElementWithExperience'; -import { WorkspaceResourceType } from '../../../tree/workspace/sharedWorkspaceResourceProvider'; -import { SharedWorkspaceStorage } from '../../../tree/workspace/sharedWorkspaceStorage'; +import { WorkspaceResourceType } from '../../../tree/workspace/SharedWorkspaceResourceProvider'; +import { SharedWorkspaceStorage } from '../../../tree/workspace/SharedWorkspaceStorage'; import { type MongoClusterModel } from '../MongoClusterModel'; import { MongoClusterWorkspaceItem } from './MongoClusterWorkspaceItem'; diff --git a/src/tree/CosmosAccountResourceItemBase.ts b/src/tree/CosmosAccountResourceItemBase.ts index 79c4b25b4..31ffd7ee2 100644 --- a/src/tree/CosmosAccountResourceItemBase.ts +++ b/src/tree/CosmosAccountResourceItemBase.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type TreeElementBase } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import { type TreeItem } from 'vscode'; import { getExperienceLabel, tryGetExperience } from '../AzureDBExperiences'; diff --git a/src/tree/CosmosDBBranchDataProvider.ts b/src/tree/CosmosDBBranchDataProvider.ts index d91aeab93..f7b27ebbb 100644 --- a/src/tree/CosmosDBBranchDataProvider.ts +++ b/src/tree/CosmosDBBranchDataProvider.ts @@ -3,11 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { - callWithTelemetryAndErrorHandling, - type IActionContext, - type TreeElementWithId, -} from '@microsoft/vscode-azext-utils'; +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; import { API, tryGetExperience } from '../AzureDBExperiences'; diff --git a/src/tree/mongo/DatabaseItem.ts b/src/tree/mongo/DatabaseItem.ts deleted file mode 100644 index 76ea0b512..000000000 --- a/src/tree/mongo/DatabaseItem.ts +++ /dev/null @@ -1,63 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createGenericElement } from '@microsoft/vscode-azext-utils'; -import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; -import { type IDatabaseInfo } from './IDatabaseInfo'; -import { type MongoAccountModel } from './MongoAccountModel'; - -export class DatabaseItem implements CosmosDBTreeElement { - id: string; - - constructor( - readonly account: MongoAccountModel, - readonly databaseInfo: IDatabaseInfo, - ) { - this.id = `${account.id}/${databaseInfo.name}`; - } - - async getChildren(): Promise { - return [ - createGenericElement({ - contextValue: 'mongoClusters.item.no-collection', - id: `${this.id}/no-databases`, - label: 'Create collection...', - commandId: 'command.mongoClusters.createCollection', - commandArgs: [this], - }) as CosmosDBTreeElement, - ]; - } - // const client: MongoClustersClient = await MongoClustersClient.getClient(this.mongoCluster.id); - // const collections = await client.listCollections(this.databaseInfo.name); - - // if (collections.length === 0) { - // // no databases in there: - // return [ - // createGenericElement({ - // contextValue: 'mongoClusters.item.no-collection', - // id: `${this.id}/no-databases`, - // label: 'Create collection...', - // iconPath: new vscode.ThemeIcon('plus'), - // commandId: 'command.mongoClusters.createCollection', - // commandArgs: [this], - // }), - // ]; - // } - - // return collections.map((collection) => { - // return new CollectionItem(this.mongoCluster, this.databaseInfo, collection); - // }); - - getTreeItem(): TreeItem { - return { - id: this.id, - contextValue: 'mongoClusters.item.database', - label: this.databaseInfo.name, - iconPath: new ThemeIcon('database'), - collapsibleState: TreeItemCollapsibleState.Collapsed, - }; - } -} diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index 5b568a2e2..268df2b38 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -4,12 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { type DatabaseAccountGetResults } from '@azure/arm-cosmosdb'; -import { - callWithTelemetryAndErrorHandling, - nonNullProp, - type IActionContext, - type TreeElementWithId, -} from '@microsoft/vscode-azext-utils'; +import { callWithTelemetryAndErrorHandling, nonNullProp, type IActionContext } from '@microsoft/vscode-azext-utils'; import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; import ConnectionString from 'mongodb-connection-string-url'; import { type Experience } from '../../AzureDBExperiences'; @@ -20,9 +15,7 @@ import { DatabaseItem } from '../../mongoClusters/tree/DatabaseItem'; import { type MongoClusterModel } from '../../mongoClusters/tree/MongoClusterModel'; import { createCosmosDBManagementClient } from '../../utils/azureClients'; import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; -import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; -import { DatabaseItem } from './DatabaseItem'; -import { type IDatabaseInfo } from './IDatabaseInfo'; +import { type CosmosDBTreeElement, type ExtTreeElementBase } from '../CosmosDBTreeElement'; import { type MongoAccountModel } from './MongoAccountModel'; /** @@ -79,6 +72,8 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { async getChildren(): Promise { ext.outputChannel.appendLine(`Cosmos DB for MongoDB (RU): Loading details for "${this.account.name}"`); + let mongoClient: MongoClustersClient | null = null; + // Check if credentials are cached, and return the cached client if available if (CredentialCache.hasCredentials(this.id)) { ext.outputChannel.appendLine( @@ -151,7 +146,7 @@ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { empty: database.empty, }; - return new DatabaseItem(clusterInfo, databaseInfo); + return new DatabaseItem(clusterInfo, databaseInfo) as ExtTreeElementBase; }); // } catch (error) { From 91c928f7e12c67314be719648e4581ca1d3fa856 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 9 Jan 2025 16:20:19 +0100 Subject: [PATCH 25/42] updated command order for mongodb/vcore --- package.json | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index db366c8da..1b6f2d88c 100644 --- a/package.json +++ b/package.json @@ -608,6 +608,11 @@ "category": "MongoDB Clusters", "command": "command.mongoClusters.removeWorkspaceConnection", "title": "Remove Connection..." + }, + { + "category": "MongoDB Clusters", + "command": "command.mongoClusters.openColllection", + "title": "Open Collection" } ], "submenus": [ @@ -1078,11 +1083,13 @@ }, { "command": "command.mongoClusters.dropCollection", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i" + "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "3@1" }, { "command": "command.mongoClusters.dropDatabase", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i" + "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "1@1" }, { "command": "command.mongoClusters.removeWorkspaceConnection", @@ -1090,52 +1097,63 @@ }, { "command": "command.mongoClusters.createCollection", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i" + "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "1@1" }, { "command": "command.mongoClusters.createDatabase", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.mongoCluster/i && viewItem =~ /(mongocluster|mongodb)/i", + "when": "viewItem =~ /treeitem.mongoCluster|mongodb.item.account/i", "group": "1@1" }, { "command": "command.mongoClusters.copyConnectionString", "when": "viewItem =~ /mongodb.item.account/i || viewItem =~ /treeitem.mongoCluster/i && viewItem =~ /(mongocluster)/i", - "group": "1@1" + "group": "2@1" }, { "command": "command.mongoClusters.importDocuments", "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "1@1" + "group": "2@1" }, { "command": "command.mongoClusters.exportDocuments", "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "1@2" + "group": "2@2" }, { - "command": "command.mongoClusters.createDocument", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", + "command": "command.mongoClusters.openColllection", + "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "1@1" }, + { + "command": "command.mongoClusters.createDocument", + "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "1@2" + }, { "command": "command.mongoClusters.launchShell", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.(mongoCluster|database|collection)/i && viewItem =~ /(mongocluster|mongodb)/i", + "when": "viewItem =~ /treeitem.mongoCluster|mongodb.item.account/i", "group": "2@1" }, { - "command": "azureDatabases.refresh", - "when" : "viewItem =~ /treeitem.mongocluster/i && viewItem =~ /(mongocluster|mongodb)/i", + "command": "command.mongoClusters.launchShell", + "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", + "group": "2@1" + }, + { + "command": "command.mongoClusters.launchShell", + "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "4@1" }, { "command": "azureDatabases.refresh", "when" : "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "4@1" + "group": "5@1" }, { "command": "azureDatabases.refresh", "when" : "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "4@1" + "group": "3@1" }, { "command": "azureDatabases.refresh", From 582f034ad862d0c8df8e624756240d11b4bf08d5 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 9 Jan 2025 18:40:04 +0100 Subject: [PATCH 26/42] feat: Launch Shell available to both MongoDB implementations. --- src/mongoClusters/CredentialCache.ts | 2 +- src/mongoClusters/MongoClustersClient.ts | 4 + src/mongoClusters/commands/launchShell.ts | 93 ++++++++++++++++------- 3 files changed, 69 insertions(+), 30 deletions(-) diff --git a/src/mongoClusters/CredentialCache.ts b/src/mongoClusters/CredentialCache.ts index d79c59457..acdf5877d 100644 --- a/src/mongoClusters/CredentialCache.ts +++ b/src/mongoClusters/CredentialCache.ts @@ -7,7 +7,7 @@ import { addAuthenticationDataToConnectionString } from './utils/connectionStrin export interface MongoClustersCredentials { mongoClusterId: string; - connectionStringWithPassword?: string; // wipe it after use + connectionStringWithPassword?: string; connectionString: string; connectionUser: string; } diff --git a/src/mongoClusters/MongoClustersClient.ts b/src/mongoClusters/MongoClustersClient.ts index 453b45138..56fa34377 100644 --- a/src/mongoClusters/MongoClustersClient.ts +++ b/src/mongoClusters/MongoClustersClient.ts @@ -130,6 +130,10 @@ export class MongoClustersClient { return CredentialCache.getCredentials(this._credentialId)?.connectionString; } + getConnectionStringWithPassword() { + return CredentialCache.getConnectionStringWithPassword(this._credentialId); + } + async listDatabases(): Promise { const rawDatabases: ListDatabasesResult = await this._mongoClient.db().admin().listDatabases(); const databases: DatabaseItemModel[] = rawDatabases.databases.filter( diff --git a/src/mongoClusters/commands/launchShell.ts b/src/mongoClusters/commands/launchShell.ts index b73d070c6..ef90a966f 100644 --- a/src/mongoClusters/commands/launchShell.ts +++ b/src/mongoClusters/commands/launchShell.ts @@ -3,57 +3,92 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { nonNullValue, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type IActionContext } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; +import { ext } from '../../extensionVariables'; +import { MongoAccountResourceItem } from '../../tree/mongo/MongoAccountResourceItem'; import { MongoClustersClient } from '../MongoClustersClient'; import { type CollectionItem } from '../tree/CollectionItem'; import { type DatabaseItem } from '../tree/DatabaseItem'; -import { MongoClusterItemBase } from '../tree/MongoClusterItemBase'; -import { type MongoClusterResourceItem } from '../tree/MongoClusterResourceItem'; -import { - addAuthenticationDataToConnectionString, - addDatabasePathToConnectionString, -} from '../utils/connectionStringHelpers'; +import { MongoClusterResourceItem } from '../tree/MongoClusterResourceItem'; +import { MongoClusterWorkspaceItem } from '../tree/workspace/MongoClusterWorkspaceItem'; + +import { ConnectionString } from 'mongodb-connection-string-url'; export async function launchShell( context: IActionContext, - node?: DatabaseItem | CollectionItem | MongoClusterResourceItem, + node?: + | DatabaseItem + | CollectionItem + | MongoClusterWorkspaceItem + | MongoClusterResourceItem + | MongoAccountResourceItem, ): Promise { - context.telemetry.properties.experience = node?.mongoCluster.dbExperience?.api; - if (!node) { throw new Error('No database or collection selected.'); } - const client: MongoClustersClient = await MongoClustersClient.getClient(node.mongoCluster.id); + let rawConnectionString: string | undefined; - const connectionString = client.getConnectionString(); - const username = client.getUserName(); + // connection string discovery for these items can be slow, so we need to run it with a temporary description + if (node instanceof MongoClusterResourceItem || node instanceof MongoAccountResourceItem) { + rawConnectionString = await ext.state.runWithTemporaryDescription(node.id, 'Working...', async () => { + if (node instanceof MongoAccountResourceItem) { + context.telemetry.properties.experience = node.experience?.api; + return node.discoverConnectionString(); + } - const connectionStringWithUserName = addAuthenticationDataToConnectionString( - nonNullValue(connectionString), - nonNullValue(username), - undefined, - ); + if (node instanceof MongoClusterResourceItem) { + context.telemetry.properties.experience = node.mongoCluster.dbExperience?.api; + return node.discoverConnectionString(); + } - let shellParameters = ''; + return undefined; + }); + // WorkspaceItems are fast as there is no connnestion string discovery happening + } else if (node instanceof MongoClusterWorkspaceItem) { + context.telemetry.properties.experience = node.mongoCluster.dbExperience?.api; + rawConnectionString = await node.discoverConnectionString(); + // TODO: add an entry work mongodb workspaceitem once ready + } // everything else has the connection string available in memory as we're connected to the server + else { + context.telemetry.properties.experience = node.experience?.api; + const client: MongoClustersClient = await MongoClustersClient.getClient(node.mongoCluster.id); + rawConnectionString = client.getConnectionStringWithPassword(); + } - if (node instanceof MongoClusterItemBase) { - shellParameters = `"${connectionStringWithUserName}"`; - } /*if (node instanceof DatabaseItem)*/ else { - const connStringWithDb = addDatabasePathToConnectionString( - connectionStringWithUserName, - node.databaseInfo.name, - ); - shellParameters = `"${connStringWithDb}"`; + if (!rawConnectionString) { + void vscode.window.showErrorMessage('Failed to extract the connection string from the selected cluster.'); + return; } + + const connectionString: ConnectionString = new ConnectionString(rawConnectionString); + + const username = connectionString.username; + const password = connectionString.password; + + const isWindows = process.platform === 'win32'; + connectionString.username = isWindows ? '%USERNAME%' : '$USERNAME'; + connectionString.password = isWindows ? '%PASSWORD%' : '$PASSWORD'; + + if ('databaseInfo' in node && node.databaseInfo?.name) { + connectionString.pathname = node.databaseInfo.name; + } + // } else if (node instanceof CollectionItem) { // --> --eval terminates, we'd have to launch with a script etc. let's look into it latter // const connStringWithDb = addDatabasePathToConnectionString(connectionStringWithUserName, node.databaseInfo.name); // shellParameters = `"${connStringWithDb}" --eval 'db.getCollection("${node.collectionInfo.name}")'` // } - const terminal: vscode.Terminal = vscode.window.createTerminal('MongoDB Clusters Shell'); + const terminal: vscode.Terminal = vscode.window.createTerminal({ + name: `MongoDB Shell (${username})`, + hideFromUser: false, + env: { + USERNAME: username, + PASSWORD: password, + }, + }); - terminal.sendText('mongosh ' + shellParameters); + terminal.sendText(`mongosh "${connectionString.toString()}"`); terminal.show(); } From 8bb68e4ecfd5db9e28c83763f5030df5cba02772 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 10 Jan 2025 14:21:57 +0100 Subject: [PATCH 27/42] feat: adding 'create database' feature to monogodb-ru --- src/mongoClusters/MongoClustersClient.ts | 2 +- src/mongoClusters/commands/createDatabase.ts | 35 ++++++++++++---- src/mongoClusters/tree/DatabaseItem.ts | 16 ++++--- .../tree/MongoClusterItemBase.ts | 19 +++++---- .../tree/MongoClustersBranchDataProvider.ts | 3 +- .../wizards/create/PromptDatabaseNameStep.ts | 2 +- .../wizards/create/createWizardContexts.ts | 3 +- src/tree/mongo/MongoAccountResourceItem.ts | 42 ++++++++++++++++++- 8 files changed, 95 insertions(+), 27 deletions(-) diff --git a/src/mongoClusters/MongoClustersClient.ts b/src/mongoClusters/MongoClustersClient.ts index 56fa34377..3cdf1cac3 100644 --- a/src/mongoClusters/MongoClustersClient.ts +++ b/src/mongoClusters/MongoClustersClient.ts @@ -363,7 +363,7 @@ export class MongoClustersClient { const newCollection = await this._mongoClient .db(databaseName) .createCollection('_dummy_collection_creation_forces_db_creation'); - await newCollection.drop(); + await newCollection.drop({ writeConcern: { w: 'majority', wtimeout: 5000 } }); } catch (_e) { console.error(_e); //todo: add to telemetry return false; diff --git a/src/mongoClusters/commands/createDatabase.ts b/src/mongoClusters/commands/createDatabase.ts index 72f49626e..8e71e034d 100644 --- a/src/mongoClusters/commands/createDatabase.ts +++ b/src/mongoClusters/commands/createDatabase.ts @@ -4,38 +4,57 @@ *--------------------------------------------------------------------------------------------*/ import { AzureWizard, nonNullValue, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { MongoAccountResourceItem } from '../../tree/mongo/MongoAccountResourceItem'; import { showConfirmationAsInSettings } from '../../utils/dialogs/showConfirmation'; import { localize } from '../../utils/localize'; import { CredentialCache } from '../CredentialCache'; -import { type MongoClusterResourceItem } from '../tree/MongoClusterResourceItem'; +import { MongoClusterItemBase } from '../tree/MongoClusterItemBase'; import { type CreateCollectionWizardContext, type CreateDatabaseWizardContext, } from '../wizards/create/createWizardContexts'; import { DatabaseNameStep } from '../wizards/create/PromptDatabaseNameStep'; -export async function createDatabase(context: IActionContext, clusterNode?: MongoClusterResourceItem): Promise { - context.telemetry.properties.experience = clusterNode?.mongoCluster.dbExperience?.api; - +export async function createDatabase( + context: IActionContext, + clusterNode?: MongoClusterItemBase | MongoAccountResourceItem, +): Promise { // node ??= ... pick a node if not provided if (!clusterNode) { throw new Error('No cluster selected.'); } - if (!CredentialCache.hasCredentials(clusterNode.mongoCluster.id)) { + let connectionId: string = ''; + let clusterName: string = ''; + + // TODO: currently MongoAccountResourceItem does not reuse MongoClusterItemBase, this will be refactored after the v1 to v2 tree migration + + if (clusterNode instanceof MongoAccountResourceItem) { + context.telemetry.properties.experience = clusterNode.experience?.api; + connectionId = clusterNode.id; + clusterName = clusterNode.account.name; + } + + if (clusterNode instanceof MongoClusterItemBase) { + context.telemetry.properties.experience = clusterNode.mongoCluster.dbExperience?.api; + connectionId = clusterNode.mongoCluster.id; + clusterName = clusterNode.mongoCluster.name; + } + + if (!CredentialCache.hasCredentials(connectionId)) { throw new Error( localize( 'mongoClusters.notSignedIn', 'You are not signed in to the MongoDB Cluster. Please sign in (by expanding the node "{0}") and try again.', - clusterNode.mongoCluster.name, + clusterName, ), ); } const wizardContext: CreateDatabaseWizardContext = { ...context, - credentialsId: clusterNode.mongoCluster.id, - mongoClusterItem: clusterNode, + credentialsId: connectionId, + clusterName: clusterName, }; const wizard: AzureWizard = new AzureWizard(wizardContext, { diff --git a/src/mongoClusters/tree/DatabaseItem.ts b/src/mongoClusters/tree/DatabaseItem.ts index b56c3052a..25c456274 100644 --- a/src/mongoClusters/tree/DatabaseItem.ts +++ b/src/mongoClusters/tree/DatabaseItem.ts @@ -74,17 +74,21 @@ export class DatabaseItem implements TreeElementWithId, TreeElementWithExperienc async createCollection(_context: IActionContext, collectionName: string): Promise { const client = await MongoClustersClient.getClient(this.mongoCluster.id); - let success = false; - - await ext.state.showCreatingChild( + return ext.state.showCreatingChild( this.id, localize('mongoClusters.tree.creating', 'Creating "{0}"...', collectionName), async () => { - success = await client.createCollection(this.databaseInfo.name, collectionName); + // Adding a delay to ensure the "creating child" animation is visible. + // The `showCreatingChild` function refreshes the parent to show the + // "creating child" animation and label. Refreshing the parent triggers its + // `getChildren` method. If the database creation completes too quickly, + // the dummy node with the animation might be shown alongside the actual + // database entry, as it will already be available in the database. + // Note to future maintainers: Do not remove this delay. + await new Promise((resolve) => setTimeout(resolve, 250)); + return client.createCollection(this.databaseInfo.name, collectionName); }, ); - - return success; } getTreeItem(): TreeItem { diff --git a/src/mongoClusters/tree/MongoClusterItemBase.ts b/src/mongoClusters/tree/MongoClusterItemBase.ts index 6a644d58f..48eabc8d5 100644 --- a/src/mongoClusters/tree/MongoClusterItemBase.ts +++ b/src/mongoClusters/tree/MongoClusterItemBase.ts @@ -124,17 +124,22 @@ export abstract class MongoClusterItemBase implements TreeElementWithId, TreeEle async createDatabase(_context: IActionContext, databaseName: string): Promise { const client = await MongoClustersClient.getClient(this.mongoCluster.id); - let success = false; - - await ext.state.showCreatingChild( + return ext.state.showCreatingChild( this.id, localize('mongoClusters.tree.creating', 'Creating "{0}"...', databaseName), - async () => { - success = await client.createDatabase(databaseName); + async (): Promise => { + // Adding a delay to ensure the "creating child" animation is visible. + // The `showCreatingChild` function refreshes the parent to show the + // "creating child" animation and label. Refreshing the parent triggers its + // `getChildren` method. If the database creation completes too quickly, + // the dummy node with the animation might be shown alongside the actual + // database entry, as it will already be available in the database. + // Note to future maintainers: Do not remove this delay. + await new Promise((resolve) => setTimeout(resolve, 250)); + + return client.createDatabase(databaseName); }, ); - - return success; } /** diff --git a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts index af07e6d0d..b6bf8e713 100644 --- a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts +++ b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts @@ -15,6 +15,7 @@ import * as vscode from 'vscode'; import { API, MongoClustersExprience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; import { createMongoClustersManagementClient } from '../../utils/azureClients'; +import { type MongoClusterItemBase } from './MongoClusterItemBase'; import { type MongoClusterModel } from './MongoClusterModel'; import { MongoClusterResourceItem } from './MongoClusterResourceItem'; @@ -115,7 +116,7 @@ export class MongoClustersBranchDataProvider ); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return ext.state.wrapItemInStateHandling(resourceItem!, () => this.refresh(resourceItem)); + return ext.state.wrapItemInStateHandling(resourceItem!, (item: MongoClusterItemBase) => this.refresh(item)); } async updateResourceCache( diff --git a/src/mongoClusters/wizards/create/PromptDatabaseNameStep.ts b/src/mongoClusters/wizards/create/PromptDatabaseNameStep.ts index 2b04523ac..9fbd09a6a 100644 --- a/src/mongoClusters/wizards/create/PromptDatabaseNameStep.ts +++ b/src/mongoClusters/wizards/create/PromptDatabaseNameStep.ts @@ -82,7 +82,7 @@ export class DatabaseNameStep extends AzureWizardPromptStep { + const client = await MongoClustersClient.getClient(this.id); + + return ext.state.showCreatingChild( + this.id, + localize('mongoClusters.tree.creating', 'Creating "{0}"...', databaseName), + async (): Promise => { + // Adding a delay to ensure the "creating child" animation is visible. + // The `showCreatingChild` function refreshes the parent to show the + // "creating child" animation and label. Refreshing the parent triggers its + // `getChildren` method. If the database creation completes too quickly, + // the dummy node with the animation might be shown alongside the actual + // database entry, as it will already be available in the database. + // Note to future maintainers: Do not remove this delay. + await new Promise((resolve) => setTimeout(resolve, 250)); + return client.createDatabase(databaseName); + }, + ); + } } From 9002b6defc4408fe9dcd018999460bfe1697c813 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 10 Jan 2025 16:21:05 +0100 Subject: [PATCH 28/42] feat: added 'delete account' option to mongodb-ru --- package.json | 2 +- .../DatabaseAccountDeleteStep.ts | 12 ++++++-- .../IDeleteWizardContext.ts | 3 +- .../deleteCosmosDBAccount.ts | 28 ++++++++++++++---- .../deleteDatabaseAccount.ts | 29 ++++++++++++++----- src/tree/CosmosAccountResourceItemBase.ts | 2 +- 6 files changed, 59 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 1b6f2d88c..6948480f1 100644 --- a/package.json +++ b/package.json @@ -703,7 +703,7 @@ }, { "command": "cosmosDB.deleteAccount", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBMongoServer(?![a-z])/i", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /mongodb.item.account(?![a-z])/i", "group": "1@2" }, { diff --git a/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts b/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts index 9f4940dd6..0f60852c0 100644 --- a/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts +++ b/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts @@ -3,14 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; +import { AzExtTreeItem, AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; +import { ext } from '../../extensionVariables'; import { type IDeleteWizardContext } from './IDeleteWizardContext'; +import { deleteCosmosDBAccount } from './deleteCosmosDBAccount'; export class DatabaseAccountDeleteStep extends AzureWizardExecuteStep { public priority: number = 100; public async execute(context: IDeleteWizardContext): Promise { - await context.node.deleteTreeItem(context); + if (context.node instanceof AzExtTreeItem) { + await context.node.deleteTreeItem(context); + } else { + await ext.state.showDeleting(context.node.id, async () => { + return deleteCosmosDBAccount(context, context.node); + }); + } } public shouldExecute(_wizardContext: IDeleteWizardContext): boolean { diff --git a/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts b/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts index a394ab799..7d790cdae 100644 --- a/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts +++ b/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts @@ -9,9 +9,10 @@ import { type IActionContext, type ISubscriptionContext, } from '@microsoft/vscode-azext-utils'; +import { type CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; export interface IDeleteWizardContext extends IActionContext, ExecuteActivityContext { - node: AzExtTreeItem; + node: AzExtTreeItem | CosmosAccountResourceItemBase; deletePostgres: boolean; resourceGroupToDelete?: string; subscription: ISubscriptionContext; diff --git a/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts b/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts index bf6f69c41..ccb1a4848 100644 --- a/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts @@ -5,18 +5,36 @@ import { type CosmosDBManagementClient } from '@azure/arm-cosmosdb'; import { getResourceGroupFromId } from '@microsoft/vscode-azext-azureutils'; -import { type AzExtTreeItem } from '@microsoft/vscode-azext-utils'; +import { AzExtTreeItem, createSubscriptionContext } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import { ext } from '../../extensionVariables'; +import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; import { createCosmosDBClient } from '../../utils/azureClients'; import { getDatabaseAccountNameFromId } from '../../utils/azureUtils'; import { localize } from '../../utils/localize'; import { type IDeleteWizardContext } from './IDeleteWizardContext'; -export async function deleteCosmosDBAccount(context: IDeleteWizardContext, node: AzExtTreeItem): Promise { - const client: CosmosDBManagementClient = await createCosmosDBClient([context, node.subscription]); - const resourceGroup: string = getResourceGroupFromId(node.fullId); - const accountName: string = getDatabaseAccountNameFromId(node.fullId); +export async function deleteCosmosDBAccount( + context: IDeleteWizardContext, + node: AzExtTreeItem | CosmosAccountResourceItemBase, +): Promise { + let client: CosmosDBManagementClient; + let resourceGroup: string; + let accountName: string; + + if (node instanceof AzExtTreeItem) { + client = await createCosmosDBClient([context, node.subscription]); + resourceGroup = getResourceGroupFromId(node.fullId); + accountName = getDatabaseAccountNameFromId(node.fullId); + } else if (node instanceof CosmosAccountResourceItemBase) { + const subscriptionContext = createSubscriptionContext(node.account.subscription); + client = await createCosmosDBClient([context, subscriptionContext]); + resourceGroup = getResourceGroupFromId(node.account.id); + accountName = node.account.name; + } else { + throw new Error('Unexpected node type'); + } + const deletePromise = client.databaseAccounts.beginDeleteAndWait(resourceGroup, accountName); if (!context.suppressNotification) { const deletingMessage: string = `Deleting account "${accountName}"...`; diff --git a/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts b/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts index bf518aafb..b1217e075 100644 --- a/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { + AzExtTreeItem, AzureWizard, + createSubscriptionContext, DeleteConfirmationStep, - type AzExtTreeItem, type IActionContext, } from '@microsoft/vscode-azext-utils'; +import { type CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; import { createActivityContext } from '../../utils/activityUtils'; import { localize } from '../../utils/localize'; import { DatabaseAccountDeleteStep } from './DatabaseAccountDeleteStep'; @@ -16,26 +18,39 @@ import { type IDeleteWizardContext } from './IDeleteWizardContext'; export async function deleteDatabaseAccount( context: IActionContext, - node: AzExtTreeItem, + node: AzExtTreeItem | CosmosAccountResourceItemBase, isPostgres: boolean = false, ): Promise { const wizardContext: IDeleteWizardContext = Object.assign(context, { node, deletePostgres: isPostgres, - subscription: node.subscription, + subscription: + node instanceof AzExtTreeItem ? node.subscription : createSubscriptionContext(node.account.subscription), ...(await createActivityContext()), }); const title = wizardContext.deletePostgres - ? localize('deletePoSer', 'Delete Postgres Server "{0}"', node.label) - : localize('deleteDbAcc', 'Delete Database Account "{0}"', node.label); + ? localize( + 'deletePoSer', + 'Delete Postgres Server "{0}"', + node instanceof AzExtTreeItem ? node.label : node.account.name, + ) + : localize( + 'deleteDbAcc', + 'Delete Database Account "{0}"', + node instanceof AzExtTreeItem ? node.label : node.account.name, + ); const confirmationMessage = wizardContext.deletePostgres - ? localize('deleteAccountConfirm', 'Are you sure you want to delete server "{0}" and its contents?', node.label) + ? localize( + 'deleteAccountConfirm', + 'Are you sure you want to delete server "{0}" and its contents?', + node instanceof AzExtTreeItem ? node.label : node.account.name, + ) : localize( 'deleteAccountConfirm', 'Are you sure you want to delete account "{0}" and its contents?', - node.label, + node instanceof AzExtTreeItem ? node.label : node.account.name, ); const wizard = new AzureWizard(wizardContext, { diff --git a/src/tree/CosmosAccountResourceItemBase.ts b/src/tree/CosmosAccountResourceItemBase.ts index 31ffd7ee2..a32bb2f29 100644 --- a/src/tree/CosmosAccountResourceItemBase.ts +++ b/src/tree/CosmosAccountResourceItemBase.ts @@ -13,7 +13,7 @@ export abstract class CosmosAccountResourceItemBase implements CosmosDBTreeEleme public id: string; public contextValue: string = 'cosmosDB.item.account'; - protected constructor(protected readonly account: CosmosAccountModel) { + protected constructor(readonly account: CosmosAccountModel) { this.id = account.id ?? ''; } From e663ddc69cb45d69d8387f2c6d5701d14fc6f274 Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Mon, 13 Jan 2025 13:31:58 +0100 Subject: [PATCH 29/42] feat: Migrating TreeView to V2 --- extension.bundle.ts | 17 +- package.json | 435 ++++++++---------- src/extensionVariables.ts | 17 +- src/tree/AzureAccountTreeItemWithAttached.ts | 36 -- src/tree/CosmosDBBranchDataProvider.ts | 29 +- .../table/TableAccountAttachedResourceItem.ts | 4 +- src/tree/table/TableAccountResourceItem.ts | 4 +- 7 files changed, 224 insertions(+), 318 deletions(-) delete mode 100644 src/tree/AzureAccountTreeItemWithAttached.ts diff --git a/extension.bundle.ts b/extension.bundle.ts index fdb1a8a29..3bb2aefcf 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -24,19 +24,28 @@ export { emulatorPassword, isWindows } from './src/constants'; export { ParsedDocDBConnectionString, parseDocDBConnectionString } from './src/docdb/docDBConnectionStrings'; export { getCosmosClient } from './src/docdb/getCosmosClient'; export * from './src/docdb/registerDocDBCommands'; -export { activateInternal, cosmosDBCopyConnectionString, createServer, deactivateInternal, deleteAccount } from './src/extension'; +export { + activateInternal, + cosmosDBCopyConnectionString, + createServer, + deactivateInternal, + deleteAccount, +} from './src/extension'; export { ext } from './src/extensionVariables'; export * from './src/graph/registerGraphCommands'; +export { connectToMongoClient, isCosmosEmulatorConnectionString } from './src/mongo/connectToMongoClient'; export { MongoCommand } from './src/mongo/MongoCommand'; +export { + addDatabaseToAccountConnectionString, + encodeMongoConnectionString, + getDatabaseNameFromConnectionString, +} from './src/mongo/mongoConnectionStrings'; export { findCommandAtPosition, getAllCommandsFromText } from './src/mongo/MongoScrapbook'; export { MongoShell } from './src/mongo/MongoShell'; -export { connectToMongoClient, isCosmosEmulatorConnectionString } from './src/mongo/connectToMongoClient'; -export { addDatabaseToAccountConnectionString, encodeMongoConnectionString, getDatabaseNameFromConnectionString } from './src/mongo/mongoConnectionStrings'; export * from './src/mongo/registerMongoCommands'; export { IDatabaseInfo } from './src/mongo/tree/MongoAccountTreeItem'; export { addDatabaseToConnectionString } from './src/postgres/postgresConnectionStrings'; export { AttachedAccountsTreeItem, MONGO_CONNECTION_EXPECTED } from './src/tree/AttachedAccountsTreeItem'; -export { AzureAccountTreeItemWithAttached } from './src/tree/AzureAccountTreeItemWithAttached'; export * from './src/utils/azureClients'; export { getPublicIpv4, isIpInRanges } from './src/utils/getIp'; export { improveError } from './src/utils/improveError'; diff --git a/package.json b/package.json index 1b6f2d88c..9f8a45a03 100644 --- a/package.json +++ b/package.json @@ -706,91 +706,11 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBMongoServer(?![a-z])/i", "group": "1@2" }, - { - "command": "cosmosDB.deleteAccount", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBDocumentServer(?![a-z])/i", - "group": "1@2" - }, - { - "command": "cosmosDB.deleteAccount", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBGraphAccount(?![a-z])/i", - "group": "1@2" - }, - { - "command": "cosmosDB.deleteAccount", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBTableAccount(?![a-z])/i", - "group": "1@2" - }, { "command": "postgreSQL.deleteServer", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /postgresServer(?![a-z])/i", "group": "1@2" }, - { - "command": "cosmosDB.createDocDBDocument", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentsGroup", - "group": "1@2" - }, - { - "command": "cosmosDB.openNoSqlQueryEditor", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection && config.cosmosDB.preview.queryEditor", - "group": "1@1" - }, - { - "command": "cosmosDB.openNoSqlQueryEditor", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentsGroup && config.cosmosDB.preview.queryEditor", - "group": "1@1" - }, - { - "command": "cosmosDB.writeNoSqlQuery", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", - "group": "1@2" - }, - { - "command": "cosmosDB.importDocument", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", - "group": "1@3" - }, - { - "command": "cosmosDB.createDocDBStoredProcedure", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBStoredProceduresGroup", - "group": "1@1" - }, - { - "command": "cosmosDB.createDocDBTrigger", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBTriggersGroup", - "group": "1@1" - }, - { - "command": "cosmosDB.createDocDBCollection", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentDatabase", - "group": "1@1" - }, - { - "command": "cosmosDB.createDocDBDatabase", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBDocumentServer(?![a-z])/i", - "group": "1@1" - }, - { - "command": "cosmosDB.createDocDBDatabase", - "when": "view == azureWorkspace && viewItem == cosmosDBDocumentServerAttached", - "group": "1@1" - }, - { - "command": "cosmosDB.createGraphDatabase", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBGraphAccount(?![a-z])/i", - "group": "1@1" - }, - { - "command": "cosmosDB.createGraphDatabase", - "when": "view == azureWorkspace && viewItem == cosmosDBGraphAccountAttached", - "group": "1@1" - }, - { - "command": "cosmosDB.createGraph", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBGraphDatabase", - "group": "1@1" - }, { "command": "postgreSQL.showPasswordlessWiki", "when": "view =~ /azure(ResourceGroups|azureFocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i && viewItem =~ /usesPassword/i", @@ -811,21 +731,6 @@ "when": "view == azureWorkspace && viewItem == cosmosDBMongoServerAttached", "group": "1@2" }, - { - "command": "azureDatabases.detachDatabaseAccount", - "when": "view == azureWorkspace && viewItem == cosmosDBGraphAccountAttached", - "group": "1@2" - }, - { - "command": "azureDatabases.detachDatabaseAccount", - "when": "view == azureWorkspace && viewItem == cosmosDBDocumentServerAttached", - "group": "1@2" - }, - { - "command": "azureDatabases.detachDatabaseAccount", - "when": "view == azureWorkspace && viewItem == cosmosDBTableAccountAttached", - "group": "1@2" - }, { "command": "azureDatabases.detachDatabaseAccount", "when": "view == azureWorkspace && viewItem == postgresServerAttached", @@ -836,51 +741,6 @@ "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == mongoDb", "group": "2@1" }, - { - "command": "cosmosDB.deleteDocDBCollection", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", - "group": "1@4" - }, - { - "command": "cosmosDB.viewDocDBCollectionOffer", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", - "group": "1@5" - }, - { - "command": "cosmosDB.viewDocDBDatabaseOffer", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentDatabase", - "group": "1@5" - }, - { - "command": "cosmosDB.deleteDocDBDocument", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocument", - "group": "1@2" - }, - { - "command": "cosmosDB.deleteDocDBStoredProcedure", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBStoredProcedure", - "group": "1@2" - }, - { - "command": "cosmosDB.executeDocDBStoredProcedure", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBStoredProcedure", - "group": "1@1" - }, - { - "command": "cosmosDB.deleteDocDBTrigger", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBTrigger", - "group": "1@2" - }, - { - "command": "cosmosDB.deleteDocDBDatabase", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentDatabase", - "group": "1@2" - }, - { - "command": "cosmosDB.deleteGraphDatabase", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBGraphDatabase", - "group": "1@2" - }, { "command": "postgreSQL.deleteDatabase", "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", @@ -901,61 +761,16 @@ "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresStoredProcedure", "group": "1@2" }, - { - "command": "cosmosDB.deleteGraph", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBGraph", - "group": "1@2" - }, - { - "command": "cosmosDB.attachDatabaseAccount", - "when": "view == azureWorkspace && viewItem =~ /cosmosDBAttachedAccounts(?![a-z])/gi", - "group": "1@1" - }, - { - "command": "cosmosDB.attachEmulator", - "when": "view == azureWorkspace && viewItem == cosmosDBAttachedAccountsWithEmulator", - "group": "1@2" - }, { "command": "cosmosDB.copyConnectionString", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBMongoServer(?![a-z])/i", "group": "2@1" }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBGraphAccount(?![a-z])/i", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBDocumentServer(?![a-z])/i", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBTableAccount(?![a-z])/i", - "group": "2@1" - }, { "command": "cosmosDB.copyConnectionString", "when": "view == azureWorkspace && viewItem == cosmosDBMongoServerAttached", "group": "2@1" }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view == azureWorkspace && viewItem == cosmosDBGraphAccountAttached", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view == azureWorkspace && viewItem == cosmosDBDocumentServerAttached", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view == azureWorkspace && viewItem == cosmosDBTableAccountAttached", - "group": "2@1" - }, { "command": "postgreSQL.copyConnectionString", "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", @@ -966,36 +781,6 @@ "when": "view == azureWorkspace && viewItem == cosmosDBMongoServerAttached", "group": "3@2" }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", - "group": "2@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentDatabase", - "group": "4@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentsGroup", - "group": "2@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBStoredProceduresGroup", - "group": "2@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBTriggersGroup", - "group": "2@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azureWorkspace/ && viewItem =~ /cosmosDBDocumentServer(?![a-z])/i", - "group": "3@2" - }, { "command": "azureDatabases.refresh", "when": "view =~ /azureWorkspace/ && viewItem =~ /postgresServer(?![a-z])/i", @@ -1021,26 +806,6 @@ "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresStoredProcedures", "group": "2@1" }, - { - "command": "azureDatabases.refresh", - "when": "view == azureWorkspace && viewItem == cosmosDBDocumentServerAttached", - "group": "3@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azureWorkspace/ && viewItem =~ /cosmosDBGraphAccount(?![a-z])/i", - "group": "3@2" - }, - { - "command": "azureDatabases.refresh", - "when": "view == azureWorkspace && viewItem == cosmosDBGraphAccountAttached", - "group": "3@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBGraphDatabase", - "group": "2@1" - }, { "command": "azureDatabases.refresh", "when": "view == azureWorkspace && viewItem == postgresServerAttached", @@ -1056,11 +821,6 @@ "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == MongoCollection", "group": "4@1" }, - { - "command": "azureDatabases.refresh", - "when": "view == azureWorkspace && viewItem =~ /^cosmosDBAttachedAccounts(?![a-z])/gi", - "group": "2@1" - }, { "command": "cosmosDB.importDocument", "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == MongoCollection", @@ -1164,6 +924,201 @@ "command": "azureDatabases.refresh", "when" : "viewItem =~ /treeitem.index/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "4@1" + }, + + + { + "//": "[Account] Create Cosmos DB database", + "command": "cosmosDB.createDocDBDatabase", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /.+[.]item[.]account(?![a-z.\\/])/i", + "group": "1@1" + }, + { + "//": "[Account] Delete Cosmos DB account", + "command": "cosmosDB.deleteAccount", + "when": "view =~ /azure(ResourceGroups|FocusView)/ && viewItem =~ /.+[.]item[.]account(?![a-z.\\/])/i", + "group": "1@2" + }, + { + "//": "[Account] Detach Cosmos DB account (workspace only)", + "command": "azureDatabases.detachDatabaseAccount", + "when": "view =~ /azureWorkspace/ && viewItem =~ /.+[.]item[.]account(?![a-z.\\/])/", + "group": "1@2" + }, + { + "//": "[Account] Connect to Cosmos DB account", + "command": "cosmosDB.copyConnectionString", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /.+[.]item[.]account(?![a-z.\\/])/i", + "group": "2@1" + }, + { + "//": "[Account] Refresh", + "command": "azureDatabases.refresh", + "when": "view =~ /azureWorkspace/ && viewItem =~ /.+[.]item[.]account(?![a-z.\\/])/i", + "group": "3@1" + }, + + + { + "//": "[Database] Create Graph container", + "command": "cosmosDB.createGraph", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /(?<=graph)[.]item[.]database(?![a-z.\\/])/i", + "group": "1@1" + }, + { + "//": "[Database] Create NoSql container", + "command": "cosmosDB.createDocDBCollection", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /(?; export let attachedAccountsNode: AttachedAccountsTreeItem; export let isBundle: boolean | undefined; - export let azureAccountTreeItem: AzureAccountTreeItemWithAttached; export let secretStorage: SecretStorage; export let postgresCodeLensProvider: PostgresCodeLensProvider | undefined; export const prefix: string = 'azureDatabases'; @@ -53,9 +43,6 @@ export namespace ext { // used for the resources tree export let mongoClustersBranchDataProvider: MongoClustersBranchDataProvider; - // used for the workspace: this is the general provider - export let workspaceDataProvider: SharedWorkspaceResourceProvider; - // used for the workspace: these are the dedicated providers export let mongoClustersWorkspaceBranchDataProvider: MongoClustersWorkspaceBranchDataProvider; diff --git a/src/tree/AzureAccountTreeItemWithAttached.ts b/src/tree/AzureAccountTreeItemWithAttached.ts deleted file mode 100644 index 226a1a43b..000000000 --- a/src/tree/AzureAccountTreeItemWithAttached.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzureAccountTreeItemBase } from '@microsoft/vscode-azext-azureutils'; -import { type AzExtTreeItem, type IActionContext, type ISubscriptionContext } from '@microsoft/vscode-azext-utils'; -import { ext } from '../extensionVariables'; -import { AttachedAccountsTreeItem } from './AttachedAccountsTreeItem'; -import { SubscriptionTreeItem } from './SubscriptionTreeItem'; - -export class AzureAccountTreeItemWithAttached extends AzureAccountTreeItemBase { - public constructor(testAccount?: object) { - super(undefined, testAccount); - ext.attachedAccountsNode = new AttachedAccountsTreeItem(this); - } - - public createSubscriptionTreeItem(root: ISubscriptionContext): SubscriptionTreeItem { - return new SubscriptionTreeItem(this, root); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - const children: AzExtTreeItem[] = await super.loadMoreChildrenImpl(clearCache, context); - return children.concat(ext.attachedAccountsNode); - } - - public compareChildrenImpl(item1: AzExtTreeItem, item2: AzExtTreeItem): number { - if (item1 instanceof AttachedAccountsTreeItem) { - return 1; - } else if (item2 instanceof AttachedAccountsTreeItem) { - return -1; - } else { - return super.compareChildrenImpl(item1, item2); - } - } -} diff --git a/src/tree/CosmosDBBranchDataProvider.ts b/src/tree/CosmosDBBranchDataProvider.ts index 4b9b1cf05..9952673dd 100644 --- a/src/tree/CosmosDBBranchDataProvider.ts +++ b/src/tree/CosmosDBBranchDataProvider.ts @@ -11,7 +11,7 @@ import { } from '@microsoft/vscode-azext-utils'; import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; -import { API, tryGetExperience } from '../AzureDBExperiences'; +import { API, CoreExperience, tryGetExperience } from '../AzureDBExperiences'; import { databaseAccountType } from '../constants'; import { ext } from '../extensionVariables'; import { localize } from '../utils/localize'; @@ -42,22 +42,12 @@ export class CosmosDBBranchDataProvider */ async getChildren(element: CosmosDBTreeElement): Promise { try { - const result = await callWithTelemetryAndErrorHandling( - 'CosmosDBBranchDataProvider.getChildren', - async (context: IActionContext) => { - context.errorHandling.suppressDisplay = true; - context.errorHandling.rethrow = true; - context.errorHandling.forceIncludeInReportIssueCommand = true; - - return (await element.getChildren?.())?.map((child) => { - return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => - this.refresh(child), - ) as CosmosDBTreeElement; - }); - }, - ); - - return result ?? []; + const children = (await element.getChildren?.()) ?? []; + return children.map((child) => { + return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => + this.refresh(child), + ) as CosmosDBTreeElement; + }); } catch (error) { return [ createGenericElement({ @@ -75,7 +65,7 @@ export class CosmosDBBranchDataProvider async getResourceItem(resource: CosmosDBResource): Promise { const resourceItem = await callWithTelemetryAndErrorHandling( 'CosmosDBBranchDataProvider.getResourceItem', - async (context: IActionContext) => { + (context: IActionContext) => { const id = nonNullProp(resource, 'id'); const name = nonNullProp(resource, 'name'); const type = nonNullProp(resource, 'type'); @@ -107,7 +97,8 @@ export class CosmosDBBranchDataProvider return new TableAccountResourceItem(accountModel, experience); } - // Unknown experience + // Unknown experience fallback + return new NoSqlAccountResourceItem(accountModel, CoreExperience); } else { // Unknown resource type } diff --git a/src/tree/table/TableAccountAttachedResourceItem.ts b/src/tree/table/TableAccountAttachedResourceItem.ts index b42e68548..6950cb746 100644 --- a/src/tree/table/TableAccountAttachedResourceItem.ts +++ b/src/tree/table/TableAccountAttachedResourceItem.ts @@ -25,9 +25,9 @@ export class TableAccountAttachedResourceItem extends DocumentDBAccountAttachedR return Promise.resolve([ createGenericElement({ - contextValue: this.contextValue, + contextValue: `${this.contextValue}/notSupported`, label: 'Table Accounts are not supported yet.', - id: `${this.id}/no-databases`, + id: `${this.id}/notSupported`, }) as CosmosDBTreeElement, ]); }); diff --git a/src/tree/table/TableAccountResourceItem.ts b/src/tree/table/TableAccountResourceItem.ts index 7cbba8686..262d89f3e 100644 --- a/src/tree/table/TableAccountResourceItem.ts +++ b/src/tree/table/TableAccountResourceItem.ts @@ -25,9 +25,9 @@ export class TableAccountResourceItem extends DocumentDBAccountResourceItem { return Promise.resolve([ createGenericElement({ - contextValue: this.contextValue, + contextValue: `${this.contextValue}/notSupported`, label: 'Table Accounts are not supported yet.', - id: `${this.id}/no-databases`, + id: `${this.id}/notSupported`, }) as CosmosDBTreeElement, ]); }); From e009f7cafec57ca498223facad13264689b1e69f Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 13 Jan 2025 16:46:26 +0100 Subject: [PATCH 30/42] feat: enabled "Open Collection" context menu for MongoDB RU+vCore --- package.json | 4 ++-- src/mongoClusters/MongoClustersExtension.ts | 16 ++++++++++++++-- .../commands/openCollectionView.ts | 18 +++++++++++++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6948480f1..a7a656af8 100644 --- a/package.json +++ b/package.json @@ -611,7 +611,7 @@ }, { "category": "MongoDB Clusters", - "command": "command.mongoClusters.openColllection", + "command": "command.mongoClusters.containerView.open", "title": "Open Collection" } ], @@ -1121,7 +1121,7 @@ "group": "2@2" }, { - "command": "command.mongoClusters.openColllection", + "command": "command.mongoClusters.containerView.open", "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "1@1" }, diff --git a/src/mongoClusters/MongoClustersExtension.ts b/src/mongoClusters/MongoClustersExtension.ts index 71b7aad59..246e55937 100644 --- a/src/mongoClusters/MongoClustersExtension.ts +++ b/src/mongoClusters/MongoClustersExtension.ts @@ -29,7 +29,7 @@ import { dropDatabase } from './commands/dropDatabase'; import { mongoClustersExportEntireCollection, mongoClustersExportQueryResults } from './commands/exportDocuments'; import { mongoClustersImportDocuments } from './commands/importDocuments'; import { launchShell } from './commands/launchShell'; -import { openCollectionView } from './commands/openCollectionView'; +import { openCollectionView, openCollectionViewInternal } from './commands/openCollectionView'; import { openDocumentView } from './commands/openDocumentView'; import { removeWorkspaceConnection } from './commands/removeWorkspaceConnection'; import { MongoClustersBranchDataProvider } from './tree/MongoClustersBranchDataProvider'; @@ -83,7 +83,19 @@ export class MongoClustersExtension implements vscode.Disposable { // using registerCommand instead of vscode.commands.registerCommand for better telemetry: // https://github.com/microsoft/vscode-azuretools/tree/main/utils#telemetry-and-error-handling - registerCommand('command.internal.mongoClusters.containerView.open', openCollectionView); + /** + * Here, opening the collection view is done in two ways: one is accessible from the tree view + * via a context menu, and the other is accessible programmatically. Both of them + * use the same underlying function to open the collection view. + * + * openCollectionView calls openCollectionViewInternal with no additional parameters. + * + * It was possible to merge the two commands into one, but it would result in code that is + * harder to understand and maintain. + */ + registerCommand('command.internal.mongoClusters.containerView.open', openCollectionViewInternal); + registerCommandWithTreeNodeUnwrapping('command.mongoClusters.containerView.open', openCollectionView); + registerCommand('command.internal.mongoClusters.documentView.open', openDocumentView); registerCommandWithTreeNodeUnwrapping('command.mongoClusters.launchShell', launchShell); diff --git a/src/mongoClusters/commands/openCollectionView.ts b/src/mongoClusters/commands/openCollectionView.ts index 7fbbf4c9a..80de2a7ef 100644 --- a/src/mongoClusters/commands/openCollectionView.ts +++ b/src/mongoClusters/commands/openCollectionView.ts @@ -8,7 +8,23 @@ import { CollectionViewController } from '../../webviews/mongoClusters/collectio import { MongoClustersSession } from '../MongoClusterSession'; import { type CollectionItem } from '../tree/CollectionItem'; -export async function openCollectionView( +export async function openCollectionView(context: IActionContext, node?: CollectionItem) { + if (!node) { + throw new Error('Invalid collection node'); + } + + context.telemetry.properties.experience = node?.mongoCluster.dbExperience?.api; + + return openCollectionViewInternal(context, { + id: node.id, + clusterId: node.mongoCluster.id, + databaseName: node.databaseInfo.name, + collectionName: node.collectionInfo.name, + collectionTreeItem: node, + }); +} + +export async function openCollectionViewInternal( _context: IActionContext, props: { id: string; From f7b14cacf43d5cb80a79b992bf66a05c6a946128 Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Tue, 14 Jan 2025 11:58:51 +0100 Subject: [PATCH 31/42] feat: Migrating TreeView to V2 --- src/tree/AttachedAccountsTreeItem.ts | 7 ++- src/tree/CosmosAccountResourceItemBase.ts | 2 +- .../CosmosDBAttachedAccountsResourceItem.ts | 58 ++++++++++++++++++- .../DocumentDBAccountAttachedResourceItem.ts | 4 +- .../docdb/DocumentDBAccountResourceItem.ts | 12 ++-- src/tree/mongo/MongoAccountResourceItem.ts | 2 +- 6 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/tree/AttachedAccountsTreeItem.ts b/src/tree/AttachedAccountsTreeItem.ts index 894bee0bc..28c11a66e 100644 --- a/src/tree/AttachedAccountsTreeItem.ts +++ b/src/tree/AttachedAccountsTreeItem.ts @@ -49,7 +49,7 @@ const localMongoConnectionString: string = 'mongodb://127.0.0.1:27017'; export class AttachedAccountsTreeItem extends AzExtParentTreeItem { public static contextValue: string = 'cosmosDBAttachedAccounts' + (isWindows ? 'WithEmulator' : 'WithoutEmulator'); public readonly contextValue: string = AttachedAccountsTreeItem.contextValue; - public readonly label: string = 'Attached Database Accounts'; + public readonly label: string = 'Attached Database Accounts (Postgres)'; public childTypeLabel: string = 'Account'; public suppressMaskLabel = true; @@ -359,7 +359,10 @@ export class AttachedAccountsTreeItem extends AzExtParentTreeItem { await ext.secretStorage.get(getSecretStorageKey(this._serviceName, id)), 'connectionString', ); - persistedAccounts.push(await this.createTreeItem(connectionString, api, label, id, isEmulator)); + // TODO: Left only Postgres, other types are moved to new tree api v2 + if (api === API.PostgresSingle || api === API.PostgresFlexible) { + persistedAccounts.push(await this.createTreeItem(connectionString, api, label, id, isEmulator)); + } }), ); } diff --git a/src/tree/CosmosAccountResourceItemBase.ts b/src/tree/CosmosAccountResourceItemBase.ts index 31ffd7ee2..4e06a9efe 100644 --- a/src/tree/CosmosAccountResourceItemBase.ts +++ b/src/tree/CosmosAccountResourceItemBase.ts @@ -13,7 +13,7 @@ export abstract class CosmosAccountResourceItemBase implements CosmosDBTreeEleme public id: string; public contextValue: string = 'cosmosDB.item.account'; - protected constructor(protected readonly account: CosmosAccountModel) { + protected constructor(public readonly account: CosmosAccountModel) { this.id = account.id ?? ''; } diff --git a/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts b/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts index 9ed64cb44..2bcdb9ea1 100644 --- a/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts +++ b/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts @@ -55,7 +55,7 @@ export class CosmosDBAttachedAccountsResourceItem implements CosmosDBTreeElement context.telemetry.properties.parentContext = this.contextValue; // TODO: remove after a few releases - await this.migrateV1AccountsToV2(); // Move accounts from the old storage format to the new one + await this.pickSupportedAccounts(); // Move accounts from the old storage format to the new one const items = await SharedWorkspaceStorage.getItems(this.id); @@ -120,6 +120,62 @@ export class CosmosDBAttachedAccountsResourceItem implements CosmosDBTreeElement ); } + protected async pickSupportedAccounts(): Promise { + return callWithTelemetryAndErrorHandling( + 'CosmosDBAttachedAccountsResourceItem.pickSupportedAccounts', + async () => { + const serviceName = 'ms-azuretools.vscode-cosmosdb.connectionStrings'; + const value: string | undefined = ext.context.globalState.get(serviceName); + + if (!value) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const accounts: (string | IPersistedAccount)[] = JSON.parse(value); + for (const account of accounts) { + let id: string; + let name: string; + let isEmulator: boolean; + let api: API; + + if (typeof account === 'string') { + // Default to Mongo if the value is a string for the sake of backwards compatibility + // (Mongo was originally the only account type that could be attached) + id = account; + name = account; + api = API.MongoDB; + isEmulator = false; + } else { + id = (account).id; + name = (account).id; + api = (account).defaultExperience; + isEmulator = (account).isEmulator ?? false; + } + + // TODO: Ignore Postgres accounts until we have a way to handle them + if (api === API.PostgresSingle || api === API.PostgresFlexible) { + continue; + } + + const connectionString: string = nonNullValue( + await ext.secretStorage.get(`${serviceName}.${id}`), + 'connectionString', + ); + + const storageItem: SharedWorkspaceStorageItem = { + id, + name, + properties: { isEmulator, api }, + secrets: [connectionString], + }; + + await SharedWorkspaceStorage.push(WorkspaceResourceType.AttachedAccounts, storageItem, true); + } + }, + ); + } + protected async migrateV1AccountsToV2(): Promise { const serviceName = 'ms-azuretools.vscode-cosmosdb.connectionStrings'; const value: string | undefined = ext.context.globalState.get(serviceName); diff --git a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts index 3a62d66b0..91e57650d 100644 --- a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts @@ -25,8 +25,8 @@ export abstract class DocumentDBAccountAttachedResourceItem implements CosmosDBT protected hasShownRbacNotification: boolean = false; protected constructor( - protected account: CosmosDBAttachedAccountModel, - protected experience: Experience, + public readonly account: CosmosDBAttachedAccountModel, + public readonly experience: Experience, ) { this.contextValue = `${experience.api}.workspace.item.account`; } diff --git a/src/tree/docdb/DocumentDBAccountResourceItem.ts b/src/tree/docdb/DocumentDBAccountResourceItem.ts index 2cd03a0cf..b0be2a086 100644 --- a/src/tree/docdb/DocumentDBAccountResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountResourceItem.ts @@ -16,20 +16,22 @@ import { createCosmosDBManagementClient } from '../../utils/azureClients'; import { localize } from '../../utils/localize'; import { nonNullProp } from '../../utils/nonNull'; import { type CosmosAccountModel } from '../CosmosAccountModel'; +import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; import { type AccountInfo } from './AccountInfo'; -export abstract class DocumentDBAccountResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.account'; +export abstract class DocumentDBAccountResourceItem extends CosmosAccountResourceItemBase { + public declare id: string; + public declare contextValue: string; // To prevent the RBAC notification from showing up multiple times protected hasShownRbacNotification: boolean = false; protected constructor( - protected account: CosmosAccountModel, - protected experience: Experience, + public readonly account: CosmosAccountModel, + public readonly experience: Experience, ) { + super(account); this.contextValue = `${experience.api}.item.account`; } diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index 268df2b38..cddfda041 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -23,7 +23,7 @@ import { type MongoAccountModel } from './MongoAccountModel'; * will only behave as expected when used in the context of an Azure Subscription. */ export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { - protected declare account: MongoAccountModel; + public declare readonly account: MongoAccountModel; constructor( account: MongoAccountModel, From f5a97637f909f64aec56657032110b8bb76dbf60 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Tue, 14 Jan 2025 14:23:56 +0100 Subject: [PATCH 32/42] feat: added "Delete Account" to MongoDB vCore --- package.json | 13 ++++-- .../DatabaseAccountDeleteStep.ts | 15 ++++++- .../IDeleteWizardContext.ts | 3 +- .../deleteDatabaseAccount.ts | 43 ++++++++++++------- .../deleteMongoClustersAccount.ts | 40 +++++++++++++++++ .../tree/MongoClusterResourceItem.ts | 2 +- 6 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts diff --git a/package.json b/package.json index a7a656af8..e5617eb72 100644 --- a/package.json +++ b/package.json @@ -706,6 +706,11 @@ "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /mongodb.item.account(?![a-z])/i", "group": "1@2" }, + { + "command": "cosmosDB.deleteAccount", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /mongoclusters/i && viewItem =~ /treeitem.mongoCluster/i", + "group": "1@2" + }, { "command": "cosmosDB.deleteAccount", "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBDocumentServer(?![a-z])/i", @@ -1147,22 +1152,22 @@ }, { "command": "azureDatabases.refresh", - "when" : "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", + "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "5@1" }, { "command": "azureDatabases.refresh", - "when" : "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", + "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "3@1" }, { "command": "azureDatabases.refresh", - "when" : "viewItem =~ /treeitem.indexes/i && viewItem =~ /(mongocluster|mongodb)/i", + "when": "viewItem =~ /treeitem.indexes/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "4@1" }, { "command": "azureDatabases.refresh", - "when" : "viewItem =~ /treeitem.index/i && viewItem =~ /(mongocluster|mongodb)/i", + "when": "viewItem =~ /treeitem.index/i && viewItem =~ /(mongocluster|mongodb)/i", "group": "4@1" } ], diff --git a/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts b/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts index 0f60852c0..18fd413e0 100644 --- a/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts +++ b/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts @@ -5,8 +5,12 @@ import { AzExtTreeItem, AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; import { ext } from '../../extensionVariables'; +import { MongoClusterItemBase } from '../../mongoClusters/tree/MongoClusterItemBase'; +import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; +import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; import { type IDeleteWizardContext } from './IDeleteWizardContext'; import { deleteCosmosDBAccount } from './deleteCosmosDBAccount'; +import { deleteMongoClustersAccount } from './deleteMongoClustersAccount'; export class DatabaseAccountDeleteStep extends AzureWizardExecuteStep { public priority: number = 100; @@ -14,10 +18,17 @@ export class DatabaseAccountDeleteStep extends AzureWizardExecuteStep { if (context.node instanceof AzExtTreeItem) { await context.node.deleteTreeItem(context); - } else { + } else if (context.node instanceof CosmosAccountResourceItemBase) { + await ext.state.showDeleting(context.node.id, async () => { + return deleteCosmosDBAccount(context, context.node as CosmosAccountResourceItemBase); + }); + } else if (context.node instanceof MongoClusterItemBase) { await ext.state.showDeleting(context.node.id, async () => { - return deleteCosmosDBAccount(context, context.node); + return deleteMongoClustersAccount(context, context.node as MongoClusterResourceItem); }); + ext.mongoClustersBranchDataProvider.refresh(); + } else { + throw new Error('Unexpected node type'); } } diff --git a/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts b/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts index 7d790cdae..6e6c91ee8 100644 --- a/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts +++ b/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts @@ -9,10 +9,11 @@ import { type IActionContext, type ISubscriptionContext, } from '@microsoft/vscode-azext-utils'; +import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; import { type CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; export interface IDeleteWizardContext extends IActionContext, ExecuteActivityContext { - node: AzExtTreeItem | CosmosAccountResourceItemBase; + node: AzExtTreeItem | CosmosAccountResourceItemBase | MongoClusterResourceItem; deletePostgres: boolean; resourceGroupToDelete?: string; subscription: ISubscriptionContext; diff --git a/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts b/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts index b1217e075..6723fe493 100644 --- a/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts @@ -9,8 +9,10 @@ import { createSubscriptionContext, DeleteConfirmationStep, type IActionContext, + type ISubscriptionContext, } from '@microsoft/vscode-azext-utils'; -import { type CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; +import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; +import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; import { createActivityContext } from '../../utils/activityUtils'; import { localize } from '../../utils/localize'; import { DatabaseAccountDeleteStep } from './DatabaseAccountDeleteStep'; @@ -18,39 +20,48 @@ import { type IDeleteWizardContext } from './IDeleteWizardContext'; export async function deleteDatabaseAccount( context: IActionContext, - node: AzExtTreeItem | CosmosAccountResourceItemBase, + node: AzExtTreeItem | CosmosAccountResourceItemBase | MongoClusterResourceItem, isPostgres: boolean = false, ): Promise { + let subscription: ISubscriptionContext; + if (node instanceof AzExtTreeItem) { + subscription = node.subscription; + } else if (node instanceof CosmosAccountResourceItemBase) { + subscription = createSubscriptionContext(node.account.subscription); + } else { + subscription = createSubscriptionContext((node as MongoClusterResourceItem).subscription); + } + + let accountName: string; + if (node instanceof AzExtTreeItem) { + accountName = node.label; + } else if (node instanceof CosmosAccountResourceItemBase) { + accountName = node.account.name; + } else { + accountName = (node as MongoClusterResourceItem).mongoCluster.name; + } + const wizardContext: IDeleteWizardContext = Object.assign(context, { node, deletePostgres: isPostgres, - subscription: - node instanceof AzExtTreeItem ? node.subscription : createSubscriptionContext(node.account.subscription), + subscription: subscription, ...(await createActivityContext()), }); const title = wizardContext.deletePostgres - ? localize( - 'deletePoSer', - 'Delete Postgres Server "{0}"', - node instanceof AzExtTreeItem ? node.label : node.account.name, - ) - : localize( - 'deleteDbAcc', - 'Delete Database Account "{0}"', - node instanceof AzExtTreeItem ? node.label : node.account.name, - ); + ? localize('deletePoSer', 'Delete Postgres Server "{0}"', accountName) + : localize('deleteDbAcc', 'Delete Database Account "{0}"', accountName); const confirmationMessage = wizardContext.deletePostgres ? localize( 'deleteAccountConfirm', 'Are you sure you want to delete server "{0}" and its contents?', - node instanceof AzExtTreeItem ? node.label : node.account.name, + accountName, ) : localize( 'deleteAccountConfirm', 'Are you sure you want to delete account "{0}" and its contents?', - node instanceof AzExtTreeItem ? node.label : node.account.name, + accountName, ); const wizard = new AzureWizard(wizardContext, { diff --git a/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts b/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts new file mode 100644 index 000000000..fa2c92e4f --- /dev/null +++ b/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { ext } from '../../extensionVariables'; +import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; +import { createMongoClustersManagementClient } from '../../utils/azureClients'; +import { localize } from '../../utils/localize'; +import { type IDeleteWizardContext } from './IDeleteWizardContext'; + +export async function deleteMongoClustersAccount( + context: IDeleteWizardContext, + node: MongoClusterResourceItem, +): Promise { + const client = createMongoClustersManagementClient(context, node.subscription); + const resourceGroup = node.mongoCluster.resourceGroup as string; + const accountName = node.mongoCluster.name; + + const deletePromise = (await client).mongoClusters.beginDeleteAndWait(resourceGroup, accountName); + if (!context.suppressNotification) { + const deletingMessage: string = `Deleting account "${accountName}"...`; + await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: deletingMessage }, + async () => { + await deletePromise; + const deleteMessage: string = localize( + 'deleteAccountMsg', + `Successfully deleted account "{0}".`, + accountName, + ); + void vscode.window.showInformationMessage(deleteMessage); + ext.outputChannel.appendLog(deleteMessage); + }, + ); + } else { + await deletePromise; + } +} diff --git a/src/mongoClusters/tree/MongoClusterResourceItem.ts b/src/mongoClusters/tree/MongoClusterResourceItem.ts index 1199a288e..b17b22d19 100644 --- a/src/mongoClusters/tree/MongoClusterResourceItem.ts +++ b/src/mongoClusters/tree/MongoClusterResourceItem.ts @@ -29,7 +29,7 @@ import ConnectionString from 'mongodb-connection-string-url'; export class MongoClusterResourceItem extends MongoClusterItemBase { constructor( - private readonly subscription: AzureSubscription, + readonly subscription: AzureSubscription, mongoCluster: MongoClusterModel, ) { super(mongoCluster); From a677716bcf7982886890c9f7fdcc15988153994e Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Wed, 15 Jan 2025 11:00:58 +0100 Subject: [PATCH 33/42] feat: Migrating TreeView to V2 --- package.json | 353 +++++++++--------- .../deleteCosmosDBAccount.ts | 9 +- .../deleteDatabaseAccount.ts | 16 +- src/mongoClusters/tree/CollectionItem.ts | 22 +- src/mongoClusters/tree/DatabaseItem.ts | 23 +- src/mongoClusters/tree/IndexItem.ts | 16 +- src/mongoClusters/tree/IndexesItem.ts | 16 +- .../tree/MongoClusterItemBase.ts | 25 +- .../workspace/MongoClusterWorkspaceItem.ts | 3 +- .../workspace/MongoDBAccountsWorkspaceItem.ts | 2 +- src/tree/CosmosAccountResourceItemBase.ts | 39 +- src/tree/CosmosDBBranchDataProvider.ts | 37 +- .../CosmosDBWorkspaceBranchDataProvider.ts | 23 +- src/tree/TreeElementWithContextValue.ts | 12 + .../CosmosDBAttachedAccountsResourceItem.ts | 35 +- .../DocumentDBAccountAttachedResourceItem.ts | 38 +- .../docdb/DocumentDBAccountResourceItem.ts | 47 +-- .../docdb/DocumentDBContainerResourceItem.ts | 37 +- .../docdb/DocumentDBDatabaseResourceItem.ts | 37 +- src/tree/docdb/DocumentDBItemResourceItem.ts | 76 ++-- src/tree/docdb/DocumentDBItemsResourceItem.ts | 57 ++- .../DocumentDBStoredProcedureResourceItem.ts | 20 +- .../DocumentDBStoredProceduresResourceItem.ts | 37 +- .../docdb/DocumentDBTriggerResourceItem.ts | 20 +- .../docdb/DocumentDBTriggersResourceItem.ts | 37 +- src/tree/mongo/MongoAccountResourceItem.ts | 5 +- 26 files changed, 550 insertions(+), 492 deletions(-) create mode 100644 src/tree/TreeElementWithContextValue.ts diff --git a/package.json b/package.json index b2a450b43..ce2a81afc 100644 --- a/package.json +++ b/package.json @@ -701,9 +701,10 @@ "when": "view == azureResourceGroups && viewItem =~ /(AzureCosmosDb|PostgreSqlServers(Standard|Flexible))/i && viewItem =~ /azureResourceTypeGroup/i", "group": "1@1" }, + { - "command": "cosmosDB.deleteAccount", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /mongodb.item.account(?![a-z])/i", + "command": "azureDatabases.detachDatabaseAccount", + "when": "view == azureWorkspace && viewItem == postgresServerAttached", "group": "1@2" }, { @@ -718,29 +719,9 @@ }, { "command": "postgreSQL.createDatabase", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /postgresServer(?![a-z])/i", - "group": "1@1" - }, - { - "command": "postgreSQL.createDatabase", - "when": "view == azureWorkspace && viewItem == postgresServerAttached", + "when": "view =~ /(azureResourceGroups|Workspace|azureFocusView)/ && viewItem =~ /postgresServer/i", "group": "1@1" }, - { - "command": "azureDatabases.detachDatabaseAccount", - "when": "view == azureWorkspace && viewItem == cosmosDBMongoServerAttached", - "group": "1@2" - }, - { - "command": "azureDatabases.detachDatabaseAccount", - "when": "view == azureWorkspace && viewItem == postgresServerAttached", - "group": "1@2" - }, - { - "command": "cosmosDB.connectMongoDB", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == mongoDb", - "group": "2@1" - }, { "command": "postgreSQL.deleteDatabase", "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", @@ -762,24 +743,24 @@ "group": "1@2" }, { - "command": "cosmosDB.copyConnectionString", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBMongoServer(?![a-z])/i", + "command": "postgreSQL.copyConnectionString", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", "group": "2@1" }, { - "command": "cosmosDB.copyConnectionString", - "when": "view == azureWorkspace && viewItem == cosmosDBMongoServerAttached", - "group": "2@1" + "command": "postgreSQL.connectDatabase", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", + "group": "1@1" }, { - "command": "postgreSQL.copyConnectionString", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", - "group": "2@1" + "command": "postgreSQL.createFunctionQuery", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresFunctions", + "group": "1@1" }, { - "command": "azureDatabases.refresh", - "when": "view == azureWorkspace && viewItem == cosmosDBMongoServerAttached", - "group": "3@2" + "command": "postgreSQL.createStoredProcedureQuery", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresStoredProcedures", + "group": "1@1" }, { "command": "azureDatabases.refresh", @@ -811,150 +792,78 @@ "when": "view == azureWorkspace && viewItem == postgresServerAttached", "group": "2@1" }, + + { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == mongoDb", - "group": "3@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == MongoCollection", - "group": "4@1" - }, - { - "command": "cosmosDB.importDocument", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == MongoCollection", - "group": "1@3" - }, - { - "command": "postgreSQL.connectDatabase", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", - "group": "1@1" - }, - { - "command": "postgreSQL.createFunctionQuery", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresFunctions", - "group": "1@1" - }, - { - "command": "postgreSQL.createStoredProcedureQuery", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresStoredProcedures", - "group": "1@1" - }, - { - "command": "command.mongoClusters.dropCollection", - "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "3@1" - }, - { - "command": "command.mongoClusters.dropDatabase", - "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "1@1" - }, - { - "command": "command.mongoClusters.removeWorkspaceConnection", - "when": "vscodeDatabases.mongoClustersSupportEnabled && view == azureWorkspace && viewItem =~ /treeitem.mongoCluster/i && viewItem =~ /(mongocluster|mongodb)/i" - }, - { - "command": "command.mongoClusters.createCollection", - "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", + "//": "[Account] Create Cosmos DB database", + "command": "cosmosDB.createDocDBDatabase", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]account(?![a-z.\\/])/i", "group": "1@1" }, { + "//": "[Account] Create Mongo DB|Cluster database", "command": "command.mongoClusters.createDatabase", - "when": "viewItem =~ /treeitem.mongoCluster|mongodb.item.account/i", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", "group": "1@1" }, { - "command": "command.mongoClusters.copyConnectionString", - "when": "viewItem =~ /mongodb.item.account/i || viewItem =~ /treeitem.mongoCluster/i && viewItem =~ /(mongocluster)/i", - "group": "2@1" + "//": "[Account] Delete Cosmos DB account", + "command": "cosmosDB.deleteAccount", + "when": "view =~ /azure(ResourceGroups|FocusView)/ && viewItem =~ /treeitem[.]account(?![a-z.\\/])/i", + "group": "1@2" }, { - "command": "command.mongoClusters.importDocuments", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "2@1" + "//": "[Account] Delete Mongo DB account", + "command": "cosmosDB.deleteAccount", + "when": "view =~ /azure(ResourceGroups|FocusView)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", + "group": "1@2" }, { - "command": "command.mongoClusters.exportDocuments", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "2@2" + "//": "[Account] Detach Cosmos DB account (workspace only)", + "command": "azureDatabases.detachDatabaseAccount", + "when": "view =~ /azureWorkspace/ && viewItem =~ /treeitem[.]account(?![a-z.\\/])/i", + "group": "1@2" }, { - "command": "command.mongoClusters.openColllection", - "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "1@1" + "//": "[Account] Detach Mongo DB account (workspace only)", + "command": "azureDatabases.detachDatabaseAccount", + "when": "view =~ /azureWorkspace/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", + "group": "1@2" }, { - "command": "command.mongoClusters.createDocument", - "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", + "//": "[Account] Detach Mongo Cluster account (workspace only)", + "command": "command.mongoClusters.removeWorkspaceConnection", + "when": "view =~ /azureWorkspace/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", "group": "1@2" }, { - "command": "command.mongoClusters.launchShell", - "when": "viewItem =~ /treeitem.mongoCluster|mongodb.item.account/i", + "//": "[Account] Copy connection string to Cosmos DB account", + "command": "cosmosDB.copyConnectionString", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]account(?![a-z.\\/])/i", "group": "2@1" }, { - "command": "command.mongoClusters.launchShell", - "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", + "//": "[Account] Copy connection string to Mongo DB account", + "command": "cosmosDB.copyConnectionString", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", "group": "2@1" }, { - "command": "command.mongoClusters.launchShell", - "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "4@1" - }, - { - "command": "azureDatabases.refresh", - "when" : "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "5@1" - }, - { - "command": "azureDatabases.refresh", - "when" : "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "3@1" - }, - { - "command": "azureDatabases.refresh", - "when" : "viewItem =~ /treeitem.indexes/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "4@1" - }, - { - "command": "azureDatabases.refresh", - "when" : "viewItem =~ /treeitem.index/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "4@1" - }, - - - { - "//": "[Account] Create Cosmos DB database", - "command": "cosmosDB.createDocDBDatabase", - "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /.+[.]item[.]account(?![a-z.\\/])/i", - "group": "1@1" - }, - { - "//": "[Account] Delete Cosmos DB account", - "command": "cosmosDB.deleteAccount", - "when": "view =~ /azure(ResourceGroups|FocusView)/ && viewItem =~ /.+[.]item[.]account(?![a-z.\\/])/i", - "group": "1@2" - }, - { - "//": "[Account] Detach Cosmos DB account (workspace only)", - "command": "azureDatabases.detachDatabaseAccount", - "when": "view =~ /azureWorkspace/ && viewItem =~ /.+[.]item[.]account(?![a-z.\\/])/", - "group": "1@2" + "//": "[Account] Copy connection string to Mongo Cluster account", + "command": "command.mongoClusters.copyConnectionString", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", + "group": "2@1" }, { - "//": "[Account] Connect to Cosmos DB account", - "command": "cosmosDB.copyConnectionString", - "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /.+[.]item[.]account(?![a-z.\\/])/i", - "group": "2@1" + "//": "[Account] Mongo DB|Cluster Launch Shell", + "command": "command.mongoClusters.launchShell", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", + "group": "2@2" }, { "//": "[Account] Refresh", "command": "azureDatabases.refresh", - "when": "view =~ /azureWorkspace/ && viewItem =~ /.+[.]item[.]account(?![a-z.\\/])/i", + "when": "view =~ /azureWorkspace/ && viewItem =~ /treeitem[.](mongoCluster|account)(?![a-z.\\/])/i", "group": "3@1" }, @@ -962,109 +871,177 @@ { "//": "[Database] Create Graph container", "command": "cosmosDB.createGraph", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /(?<=graph)[.]item[.]database(?![a-z.\\/])/i", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i && viewItem =~ /experience[.](graph)/i", "group": "1@1" }, { "//": "[Database] Create NoSql container", "command": "cosmosDB.createDocDBCollection", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /(? { + let subscription: ISubscriptionContext; + if (node instanceof AzExtTreeItem) { + subscription = node.subscription; + } else if ('subscription' in node.account) { + subscription = createSubscriptionContext(node.account.subscription as AzureSubscription); + } else { + // Not all CosmosAccountResourceItemBase instances have a subscription property (attached account does not), + // so we need to create a subscription context + throw new Error('Subscription is required to delete an account.'); + } + const wizardContext: IDeleteWizardContext = Object.assign(context, { node, deletePostgres: isPostgres, - subscription: - node instanceof AzExtTreeItem ? node.subscription : createSubscriptionContext(node.account.subscription), + subscription: subscription, ...(await createActivityContext()), }); diff --git a/src/mongoClusters/tree/CollectionItem.ts b/src/mongoClusters/tree/CollectionItem.ts index 2da6162c0..bdca220d6 100644 --- a/src/mongoClusters/tree/CollectionItem.ts +++ b/src/mongoClusters/tree/CollectionItem.ts @@ -11,22 +11,26 @@ import { type TreeElementWithId, } from '@microsoft/vscode-azext-utils'; import { type Document } from 'bson'; -import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type Experience } from '../../AzureDBExperiences'; +import { ThemeIcon, type TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { API, type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; +import { type TreeElementWithContextValue } from '../../tree/TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { - MongoClustersClient, type CollectionItemModel, type DatabaseItemModel, type InsertDocumentsResult, + MongoClustersClient, } from '../MongoClustersClient'; import { IndexesItem } from './IndexesItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class CollectionItem implements TreeElementWithId, TreeElementWithExperience { - id: string; - experience?: Experience; +export class CollectionItem implements TreeElementWithId, TreeElementWithExperience, TreeElementWithContextValue { + public readonly id: string; + public readonly experience?: Experience; + public readonly contextValue: string = 'treeItem.collection'; + + private readonly experienceContextValue: string = ''; constructor( readonly mongoCluster: MongoClusterModel, @@ -35,12 +39,14 @@ export class CollectionItem implements TreeElementWithId, TreeElementWithExperie ) { this.id = `${mongoCluster.id}/${databaseInfo.name}/${collectionInfo.name}`; this.experience = mongoCluster.dbExperience; + this.experienceContextValue = `experience.${this.experience?.api ?? API.Common}`; + this.contextValue = createContextValue([this.contextValue, this.experienceContextValue]); } async getChildren(): Promise { return [ createGenericElement({ - contextValue: createContextValue(['treeitem.documents', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: createContextValue(['treeItem.documents', this.experienceContextValue]), id: `${this.id}/documents`, label: 'Documents', commandId: 'command.internal.mongoClusters.containerView.open', @@ -90,7 +96,7 @@ export class CollectionItem implements TreeElementWithId, TreeElementWithExperie getTreeItem(): TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.collection', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: this.collectionInfo.name, iconPath: new ThemeIcon('folder-opened'), collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/DatabaseItem.ts b/src/mongoClusters/tree/DatabaseItem.ts index 25c456274..a66b80818 100644 --- a/src/mongoClusters/tree/DatabaseItem.ts +++ b/src/mongoClusters/tree/DatabaseItem.ts @@ -12,17 +12,21 @@ import { } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type Experience } from '../../AzureDBExperiences'; +import { API, type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; +import { type TreeElementWithContextValue } from '../../tree/TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { localize } from '../../utils/localize'; import { MongoClustersClient, type DatabaseItemModel } from '../MongoClustersClient'; import { CollectionItem } from './CollectionItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class DatabaseItem implements TreeElementWithId, TreeElementWithExperience { - id: string; - experience?: Experience; +export class DatabaseItem implements TreeElementWithId, TreeElementWithExperience, TreeElementWithContextValue { + public readonly id: string; + public readonly experience?: Experience; + public readonly contextValue: string = 'treeItem.database'; + + private readonly experienceContextValue: string = ''; constructor( readonly mongoCluster: MongoClusterModel, @@ -30,6 +34,8 @@ export class DatabaseItem implements TreeElementWithId, TreeElementWithExperienc ) { this.id = `${mongoCluster.id}/${databaseInfo.name}`; this.experience = mongoCluster.dbExperience; + this.experienceContextValue = `experience.${this.experience?.api ?? API.Common}`; + this.contextValue = createContextValue([this.contextValue, this.experienceContextValue]); } async getChildren(): Promise { @@ -40,11 +46,8 @@ export class DatabaseItem implements TreeElementWithId, TreeElementWithExperienc // no databases in there: return [ createGenericElement({ - contextValue: createContextValue([ - 'treeitem.no-collections', - this.mongoCluster.dbExperience?.api ?? '', - ]), - id: `${this.id}/no-databases`, + contextValue: createContextValue(['treeItem.no-collections', this.experienceContextValue]), + id: `${this.id}/no-collections`, label: 'Create collection...', iconPath: new vscode.ThemeIcon('plus'), commandId: 'command.mongoClusters.createCollection', @@ -94,7 +97,7 @@ export class DatabaseItem implements TreeElementWithId, TreeElementWithExperienc getTreeItem(): TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.database', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: this.databaseInfo.name, iconPath: new ThemeIcon('database'), // TODO: create our own icon here, this one's shape can change collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/IndexItem.ts b/src/mongoClusters/tree/IndexItem.ts index c15e3486e..d4bd544c2 100644 --- a/src/mongoClusters/tree/IndexItem.ts +++ b/src/mongoClusters/tree/IndexItem.ts @@ -10,14 +10,18 @@ import { type TreeElementWithId, } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type Experience } from '../../AzureDBExperiences'; +import { API, type Experience } from '../../AzureDBExperiences'; +import { type TreeElementWithContextValue } from '../../tree/TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { type CollectionItemModel, type DatabaseItemModel, type IndexItemModel } from '../MongoClustersClient'; import { type MongoClusterModel } from './MongoClusterModel'; -export class IndexItem implements TreeElementWithId, TreeElementWithExperience { - id: string; - experience?: Experience; +export class IndexItem implements TreeElementWithId, TreeElementWithExperience, TreeElementWithContextValue { + public readonly id: string; + public readonly experience?: Experience; + public readonly contextValue: string = 'treeItem.index'; + + private readonly experienceContextValue: string = ''; constructor( readonly mongoCluster: MongoClusterModel, @@ -27,6 +31,8 @@ export class IndexItem implements TreeElementWithId, TreeElementWithExperience { ) { this.id = `${mongoCluster.id}/${databaseInfo.name}/${collectionInfo.name}/indexes/${indexInfo.name}`; this.experience = mongoCluster.dbExperience; + this.experienceContextValue = `experience.${this.experience?.api ?? API.Common}`; + this.contextValue = createContextValue([this.contextValue, this.experienceContextValue]); } async getChildren(): Promise { @@ -47,7 +53,7 @@ export class IndexItem implements TreeElementWithId, TreeElementWithExperience { getTreeItem(): TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.index', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: this.indexInfo.name, iconPath: new ThemeIcon('combine'), // TODO: create our onw icon here, this one's shape can change collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/IndexesItem.ts b/src/mongoClusters/tree/IndexesItem.ts index efa7575b0..303bcaf60 100644 --- a/src/mongoClusters/tree/IndexesItem.ts +++ b/src/mongoClusters/tree/IndexesItem.ts @@ -5,15 +5,19 @@ import { createContextValue, type TreeElementBase, type TreeElementWithId } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type Experience } from '../../AzureDBExperiences'; +import { API, type Experience } from '../../AzureDBExperiences'; +import { type TreeElementWithContextValue } from '../../tree/TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { MongoClustersClient, type CollectionItemModel, type DatabaseItemModel } from '../MongoClustersClient'; import { IndexItem } from './IndexItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class IndexesItem implements TreeElementWithId, TreeElementWithExperience { - id: string; - experience?: Experience; +export class IndexesItem implements TreeElementWithId, TreeElementWithExperience, TreeElementWithContextValue { + public readonly id: string; + public readonly experience?: Experience; + public readonly contextValue: string = 'treeItem.indexes'; + + private readonly experienceContextValue: string = ''; constructor( readonly mongoCluster: MongoClusterModel, @@ -22,6 +26,8 @@ export class IndexesItem implements TreeElementWithId, TreeElementWithExperience ) { this.id = `${mongoCluster.id}/${databaseInfo.name}/${collectionInfo.name}/indexes`; this.experience = mongoCluster.dbExperience; + this.experienceContextValue = `experience.${this.experience?.api ?? API.Common}`; + this.contextValue = createContextValue([this.contextValue, this.experienceContextValue]); } async getChildren(): Promise { @@ -35,7 +41,7 @@ export class IndexesItem implements TreeElementWithId, TreeElementWithExperience getTreeItem(): TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.indexes', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: 'Indexes', iconPath: new ThemeIcon('combine'), // TODO: create our onw icon here, this one's shape can change collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/MongoClusterItemBase.ts b/src/mongoClusters/tree/MongoClusterItemBase.ts index 48eabc8d5..24e474b3f 100644 --- a/src/mongoClusters/tree/MongoClusterItemBase.ts +++ b/src/mongoClusters/tree/MongoClusterItemBase.ts @@ -13,8 +13,9 @@ import { import { type TreeItem } from 'vscode'; import * as vscode from 'vscode'; -import { type Experience } from '../../AzureDBExperiences'; +import { API, type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; +import { type TreeElementWithContextValue } from '../../tree/TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { localize } from '../../utils/localize'; import { regionToDisplayName } from '../../utils/regionToDisplayName'; @@ -24,13 +25,20 @@ import { DatabaseItem } from './DatabaseItem'; import { type MongoClusterModel } from './MongoClusterModel'; // This info will be available at every level in the tree for immediate access -export abstract class MongoClusterItemBase implements TreeElementWithId, TreeElementWithExperience { - id: string; - experience?: Experience; +export abstract class MongoClusterItemBase + implements TreeElementWithId, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly experience?: Experience; + public readonly contextValue: string = 'treeItem.mongoCluster'; - constructor(public mongoCluster: MongoClusterModel) { + private readonly experienceContextValue: string = ''; + + protected constructor(public mongoCluster: MongoClusterModel) { this.id = mongoCluster.id ?? ''; this.experience = mongoCluster.dbExperience; + this.experienceContextValue = `experience.${this.experience?.api ?? API.Common}`; + this.contextValue = createContextValue([this.contextValue, this.experienceContextValue]); } /** @@ -97,10 +105,7 @@ export abstract class MongoClusterItemBase implements TreeElementWithId, TreeEle if (databases.length === 0) { return [ createGenericElement({ - contextValue: createContextValue([ - 'treeitem.no-databases', - this.mongoCluster.dbExperience?.api ?? '', - ]), + contextValue: createContextValue(['treeItem.no-databases', this.experienceContextValue]), id: `${this.id}/no-databases`, label: 'Create database...', iconPath: new vscode.ThemeIcon('plus'), @@ -149,7 +154,7 @@ export abstract class MongoClusterItemBase implements TreeElementWithId, TreeEle getTreeItem(): TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.mongocluster', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: this.mongoCluster.name, description: this.mongoCluster.sku !== undefined ? `(${this.mongoCluster.sku})` : false, // iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg'), // Uncomment if icon is available diff --git a/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts index a0287d507..840f83b90 100644 --- a/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts @@ -6,7 +6,6 @@ import { AzureWizard, callWithTelemetryAndErrorHandling, - createContextValue, nonNullProp, nonNullValue, UserCancelledError, @@ -159,7 +158,7 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase { getTreeItem(): vscode.TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.mongocluster', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: this.mongoCluster.name, description: this.mongoCluster.sku !== undefined ? `(${this.mongoCluster.sku})` : false, iconPath: new vscode.ThemeIcon('server-environment'), // Uncomment if icon is available diff --git a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts index 8e10d2cb1..5697d0620 100644 --- a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts @@ -35,7 +35,7 @@ export class MongoDBAccountsWorkspaceItem implements TreeElementWithId, TreeElem return new MongoClusterWorkspaceItem(model); }), createGenericElement({ - contextValue: 'treeitem.newConnection', + contextValue: 'treeItem.newConnection', id: this.id + '/newConnection', label: 'New Connection...', iconPath: new ThemeIcon('plus'), diff --git a/src/tree/CosmosAccountResourceItemBase.ts b/src/tree/CosmosAccountResourceItemBase.ts index 4e06a9efe..e3975ccfa 100644 --- a/src/tree/CosmosAccountResourceItemBase.ts +++ b/src/tree/CosmosAccountResourceItemBase.ts @@ -3,18 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { createContextValue } from '@microsoft/vscode-azext-utils'; +import { type ResourceBase } from '@microsoft/vscode-azureresources-api'; +import { v4 as uuid } from 'uuid'; import * as vscode from 'vscode'; import { type TreeItem } from 'vscode'; -import { getExperienceLabel, tryGetExperience } from '../AzureDBExperiences'; -import { type CosmosAccountModel } from './CosmosAccountModel'; +import { type Experience } from '../AzureDBExperiences'; import { type CosmosDBTreeElement } from './CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from './TreeElementWithContextValue'; +import { type TreeElementWithExperience } from './TreeElementWithExperience'; -export abstract class CosmosAccountResourceItemBase implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.account'; +export abstract class CosmosAccountResourceItemBase + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.account'; - protected constructor(public readonly account: CosmosAccountModel) { - this.id = account.id ?? ''; + protected constructor( + public readonly account: ResourceBase, + public readonly experience: Experience, + ) { + this.id = account.id ?? uuid(); + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } /** @@ -30,22 +40,11 @@ export abstract class CosmosAccountResourceItemBase implements CosmosDBTreeEleme * @returns The TreeItem object. */ getTreeItem(): TreeItem { - const experience = tryGetExperience(this.account); - if (!experience) { - const accountKindLabel = getExperienceLabel(this.account); - const label: string = this.account.name + (accountKindLabel ? ` (${accountKindLabel})` : ``); - return { - id: this.id, - contextValue: 'cosmosDB.item.account', - label: label, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - }; - } return { id: this.id, - contextValue: `${experience.api}.item.account`, + contextValue: this.contextValue, label: this.account.name, - description: `(${experience.shortName})`, + description: `(${this.experience.shortName})`, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }; } diff --git a/src/tree/CosmosDBBranchDataProvider.ts b/src/tree/CosmosDBBranchDataProvider.ts index 9952673dd..ca1be4315 100644 --- a/src/tree/CosmosDBBranchDataProvider.ts +++ b/src/tree/CosmosDBBranchDataProvider.ts @@ -22,6 +22,8 @@ import { GraphAccountResourceItem } from './graph/GraphAccountResourceItem'; import { MongoAccountResourceItem } from './mongo/MongoAccountResourceItem'; import { NoSqlAccountResourceItem } from './nosql/NoSqlAccountResourceItem'; import { TableAccountResourceItem } from './table/TableAccountResourceItem'; +import { isTreeElementWithContextValue } from './TreeElementWithContextValue'; +import { isTreeElementWithExperience } from './TreeElementWithExperience'; export class CosmosDBBranchDataProvider extends vscode.Disposable @@ -42,12 +44,35 @@ export class CosmosDBBranchDataProvider */ async getChildren(element: CosmosDBTreeElement): Promise { try { - const children = (await element.getChildren?.()) ?? []; - return children.map((child) => { - return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => - this.refresh(child), - ) as CosmosDBTreeElement; - }); + const result = await callWithTelemetryAndErrorHandling( + 'CosmosDBBranchDataProvider.getChildren', + async (context: IActionContext) => { + context.errorHandling.suppressDisplay = true; + context.errorHandling.rethrow = true; + context.errorHandling.forceIncludeInReportIssueCommand = true; + + if (isTreeElementWithContextValue(element)) { + context.telemetry.properties.parentContext = element.contextValue; + } + + if (isTreeElementWithExperience(element)) { + context.telemetry.properties.experience = element.experience?.api ?? API.Common; + } + + // TODO: values to mask. New TreeElements do not have valueToMask field + // I assume this array should be filled after element.getChildren() call + // And these values should be masked in the context + + const children = (await element.getChildren?.()) ?? []; + return children.map((child) => { + return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => + this.refresh(child), + ) as CosmosDBTreeElement; + }); + }, + ); + + return result ?? []; } catch (error) { return [ createGenericElement({ diff --git a/src/tree/CosmosDBWorkspaceBranchDataProvider.ts b/src/tree/CosmosDBWorkspaceBranchDataProvider.ts index 5fe67fe8c..6202cd22d 100644 --- a/src/tree/CosmosDBWorkspaceBranchDataProvider.ts +++ b/src/tree/CosmosDBWorkspaceBranchDataProvider.ts @@ -11,11 +11,14 @@ import { } from '@microsoft/vscode-azext-utils'; import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; +import { API } from '../AzureDBExperiences'; import { ext } from '../extensionVariables'; import { localize } from '../utils/localize'; +import { CosmosDBAttachedAccountsResourceItem } from './attached/CosmosDBAttachedAccountsResourceItem'; import { type CosmosDBResource } from './CosmosAccountModel'; import { type CosmosDBTreeElement } from './CosmosDBTreeElement'; -import { CosmosDBAttachedAccountsResourceItem } from './attached/CosmosDBAttachedAccountsResourceItem'; +import { isTreeElementWithContextValue } from './TreeElementWithContextValue'; +import { isTreeElementWithExperience } from './TreeElementWithExperience'; export class CosmosDBWorkspaceBranchDataProvider extends vscode.Disposable @@ -40,8 +43,24 @@ export class CosmosDBWorkspaceBranchDataProvider 'CosmosDBWorkspaceBranchDataProvider.getChildren', async (context: IActionContext) => { context.telemetry.properties.view = 'workspace'; + context.errorHandling.suppressDisplay = true; + context.errorHandling.rethrow = true; + context.errorHandling.forceIncludeInReportIssueCommand = true; + + if (isTreeElementWithContextValue(element)) { + context.telemetry.properties.parentContext = element.contextValue; + } + + if (isTreeElementWithExperience(element)) { + context.telemetry.properties.experience = element.experience?.api ?? API.Common; + } + + // TODO: values to mask. New TreeElements do not have valueToMask field + // I assume this array should be filled after element.getChildren() call + // And these values should be masked in the context - return (await element.getChildren?.())?.map((child) => { + const children = (await element.getChildren?.()) ?? []; + return children.map((child) => { return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => this.refresh(child), ) as CosmosDBTreeElement; diff --git a/src/tree/TreeElementWithContextValue.ts b/src/tree/TreeElementWithContextValue.ts new file mode 100644 index 000000000..a7bed5d25 --- /dev/null +++ b/src/tree/TreeElementWithContextValue.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export type TreeElementWithContextValue = { + readonly contextValue: string; +}; + +export function isTreeElementWithContextValue(node: unknown): node is TreeElementWithContextValue { + return typeof node === 'object' && node !== null && 'contextValue' in node; +} diff --git a/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts b/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts index 2bcdb9ea1..c981f0c08 100644 --- a/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts +++ b/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts @@ -5,9 +5,9 @@ import { callWithTelemetryAndErrorHandling, + createContextValue, createGenericElement, nonNullValue, - type IActionContext, } from '@microsoft/vscode-azext-utils'; import vscode, { ThemeIcon, TreeItemCollapsibleState } from 'vscode'; import { API, getExperienceFromApi } from '../../AzureDBExperiences'; @@ -18,18 +18,22 @@ import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; import { GraphAccountAttachedResourceItem } from '../graph/GraphAccountAttachedResourceItem'; import { NoSqlAccountAttachedResourceItem } from '../nosql/NoSqlAccountAttachedResourceItem'; import { TableAccountAttachedResourceItem } from '../table/TableAccountAttachedResourceItem'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; import { WorkspaceResourceType } from '../workspace/SharedWorkspaceResourceProvider'; import { SharedWorkspaceStorage, type SharedWorkspaceStorageItem } from '../workspace/SharedWorkspaceStorage'; import { type CosmosDBAttachedAccountModel } from './CosmosDBAttachedAccountModel'; -export class CosmosDBAttachedAccountsResourceItem implements CosmosDBTreeElement { - public id: string = WorkspaceResourceType.AttachedAccounts; - public contextValue: string = 'cosmosDB.workspace.item.accounts'; +export class CosmosDBAttachedAccountsResourceItem implements CosmosDBTreeElement, TreeElementWithContextValue { + public readonly id: string = WorkspaceResourceType.AttachedAccounts; + public readonly contextValue: string = 'treeItem.accounts'; private readonly attachDatabaseAccount: CosmosDBTreeElement; private readonly attachEmulator: CosmosDBTreeElement; constructor() { + this.id = WorkspaceResourceType.AttachedAccounts; + this.contextValue = createContextValue([this.contextValue, `attachedAccounts`]); + this.attachDatabaseAccount = createGenericElement({ id: `${this.id}/attachAccount`, contextValue: `${this.contextValue}/attachAccount`, @@ -50,31 +54,20 @@ export class CosmosDBAttachedAccountsResourceItem implements CosmosDBTreeElement } public async getChildren(): Promise { - const items = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.view = 'workspace'; - context.telemetry.properties.parentContext = this.contextValue; - - // TODO: remove after a few releases - await this.pickSupportedAccounts(); // Move accounts from the old storage format to the new one - - const items = await SharedWorkspaceStorage.getItems(this.id); - - return await this.getChildrenImpl(items); - }); + // TODO: remove after a few releases + await this.pickSupportedAccounts(); // Move accounts from the old storage format to the new one + const items = await SharedWorkspaceStorage.getItems(this.id); + const children = await this.getChildrenImpl(items); const auxItems = isWindows ? [this.attachDatabaseAccount, this.attachEmulator] : [this.attachDatabaseAccount]; - const result: CosmosDBTreeElement[] = []; - result.push(...(items ?? [])); - result.push(...auxItems); - - return result; + return [...children, ...auxItems]; } public getTreeItem() { return { id: this.id, - contextValue: 'cosmosDB.workspace.item.accounts', + contextValue: this.contextValue, label: 'Attached Database Accounts', iconPath: new ThemeIcon('plug'), collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts index 91e57650d..93691ae1f 100644 --- a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts @@ -14,48 +14,30 @@ import { getSignedInPrincipalIdForAccountEndpoint } from '../../docdb/utils/azur import { isRbacException, showRbacPermissionError } from '../../docdb/utils/rbacUtils'; import { localize } from '../../utils/localize'; import { type CosmosDBAttachedAccountModel } from '../attached/CosmosDBAttachedAccountModel'; +import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; import { type AccountInfo } from './AccountInfo'; -export abstract class DocumentDBAccountAttachedResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.workspace.item.account'; +export abstract class DocumentDBAccountAttachedResourceItem extends CosmosAccountResourceItemBase { + public declare readonly account: CosmosDBAttachedAccountModel; // To prevent the RBAC notification from showing up multiple times protected hasShownRbacNotification: boolean = false; - protected constructor( - public readonly account: CosmosDBAttachedAccountModel, - public readonly experience: Experience, - ) { - this.contextValue = `${experience.api}.workspace.item.account`; + protected constructor(account: CosmosDBAttachedAccountModel, experience: Experience) { + super(account, experience); } public async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; - - const accountInfo = await this.getAccountInfo(this.account); - const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); - const databases = await this.getDatabases(accountInfo, cosmosClient); - return await this.getChildrenImpl(accountInfo, databases); - }); + const accountInfo = await this.getAccountInfo(this.account); + const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); + const databases = await this.getDatabases(accountInfo, cosmosClient); - return result ?? []; + return this.getChildrenImpl(accountInfo, databases); } public getTreeItem(): TreeItem { - // This function is a bit easier than the ancestor's getTreeItem function - return { - id: this.id, - contextValue: this.contextValue, - iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg'), - label: this.account.name, - description: `(${this.experience.shortName})`, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - }; + return { ...super.getTreeItem(), iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg') }; } protected async getAccountInfo(account: CosmosDBAttachedAccountModel): Promise | never { diff --git a/src/tree/docdb/DocumentDBAccountResourceItem.ts b/src/tree/docdb/DocumentDBAccountResourceItem.ts index b0be2a086..968a1b1f6 100644 --- a/src/tree/docdb/DocumentDBAccountResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountResourceItem.ts @@ -9,6 +9,7 @@ import { type CosmosClient, type DatabaseDefinition, type Resource } from '@azur import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; +import { getThemeAgnosticIconPath } from '../../constants'; import { type CosmosDBCredential, type CosmosDBKeyCredential, getCosmosClient } from '../../docdb/getCosmosClient'; import { getSignedInPrincipalIdForAccountEndpoint } from '../../docdb/utils/azureSessionHelper'; import { ensureRbacPermissionV2, isRbacException, showRbacPermissionError } from '../../docdb/utils/rbacUtils'; @@ -21,51 +22,39 @@ import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; import { type AccountInfo } from './AccountInfo'; export abstract class DocumentDBAccountResourceItem extends CosmosAccountResourceItemBase { - public declare id: string; - public declare contextValue: string; + public declare readonly account: CosmosAccountModel; // To prevent the RBAC notification from showing up multiple times protected hasShownRbacNotification: boolean = false; - protected constructor( - public readonly account: CosmosAccountModel, - public readonly experience: Experience, - ) { - super(account); - this.contextValue = `${experience.api}.item.account`; + protected constructor(account: CosmosAccountModel, experience: Experience) { + super(account, experience); } public async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; - - const accountInfo = await this.getAccountInfo(context, this.account); - const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); - const databases = await this.getDatabases(accountInfo, cosmosClient); - return await this.getChildrenImpl(accountInfo, databases); - }); + const accountInfo = await this.getAccountInfo(this.account); + const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); + const databases = await this.getDatabases(accountInfo, cosmosClient); - return result ?? []; + return this.getChildrenImpl(accountInfo, databases); } public getTreeItem(): TreeItem { - // This function is a bit easier than the ancestor's getTreeItem function - return { - id: this.id, - contextValue: this.contextValue, - label: this.account.name, - description: `(${this.experience.shortName})`, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - }; + return { ...super.getTreeItem(), iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg') }; } - protected async getAccountInfo(context: IActionContext, account: CosmosAccountModel): Promise | never { + protected async getAccountInfo(account: CosmosAccountModel): Promise | never { const id = nonNullProp(account, 'id'); const name = nonNullProp(account, 'name'); const resourceGroup = nonNullProp(account, 'resourceGroup'); - const client = await createCosmosDBManagementClient(context, account.subscription); + + const client = await callWithTelemetryAndErrorHandling('getAccountInfo', async (context: IActionContext) => { + return createCosmosDBManagementClient(context, account.subscription); + }); + + if (!client) { + throw new Error('Failed to connect to Cosmos DB account'); + } const databaseAccount = await client.databaseAccounts.get(resourceGroup, name); const credentials = await this.getCredentials(name, resourceGroup, client, databaseAccount); diff --git a/src/tree/docdb/DocumentDBContainerResourceItem.ts b/src/tree/docdb/DocumentDBContainerResourceItem.ts index da22b28a5..7714a786f 100644 --- a/src/tree/docdb/DocumentDBContainerResourceItem.ts +++ b/src/tree/docdb/DocumentDBContainerResourceItem.ts @@ -3,39 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBContainerModel } from './models/DocumentDBContainerModel'; -export abstract class DocumentDBContainerResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.container'; +export abstract class DocumentDBContainerResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.container'; protected constructor( - protected readonly model: DocumentDBContainerModel, - protected readonly experience: Experience, + public readonly model: DocumentDBContainerModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.container`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; + const triggers = await this.getChildrenTriggersImpl(); + const storedProcedures = await this.getChildrenStoredProceduresImpl(); + const items = await this.getChildrenItemsImpl(); - const triggers = await this.getChildrenTriggersImpl(); - const storedProcedures = await this.getChildrenStoredProceduresImpl(); - const items = await this.getChildrenItemsImpl(); - - return [items, storedProcedures, triggers].filter((r) => r !== undefined); - }); - - return result ?? []; + return [items, storedProcedures, triggers].filter((r) => r !== undefined); } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBDatabaseResourceItem.ts b/src/tree/docdb/DocumentDBDatabaseResourceItem.ts index 20f7e561a..c199ab92b 100644 --- a/src/tree/docdb/DocumentDBDatabaseResourceItem.ts +++ b/src/tree/docdb/DocumentDBDatabaseResourceItem.ts @@ -4,40 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import { type ContainerDefinition, type CosmosClient, type Resource } from '@azure/cosmos'; -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { getCosmosClient } from '../../docdb/getCosmosClient'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBDatabaseModel } from './models/DocumentDBDatabaseModel'; -export abstract class DocumentDBDatabaseResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.database'; +export abstract class DocumentDBDatabaseResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.database'; protected constructor( - protected readonly model: DocumentDBDatabaseModel, - protected readonly experience: Experience, + public readonly model: DocumentDBDatabaseModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.database`; + this.id = `${model.accountInfo.id}/${model.database.id}`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + const containers = await this.getContainers(cosmosClient); - const { endpoint, credentials, isEmulator } = this.model.accountInfo; - const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); - const containers = await this.getContainers(cosmosClient); - - return await this.getChildrenImpl(containers); - }); - - return result ?? []; + return this.getChildrenImpl(containers); } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBItemResourceItem.ts b/src/tree/docdb/DocumentDBItemResourceItem.ts index ea06ccd17..8db80c69a 100644 --- a/src/tree/docdb/DocumentDBItemResourceItem.ts +++ b/src/tree/docdb/DocumentDBItemResourceItem.ts @@ -3,26 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { extractPartitionKey, getDocumentId } from '../../utils/document'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBItemModel } from './models/DocumentDBItemModel'; -export abstract class DocumentDBItemResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.item'; +export abstract class DocumentDBItemResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.document'; protected constructor( - protected readonly model: DocumentDBItemModel, - protected readonly experience: Experience, + public readonly model: DocumentDBItemModel, + public readonly experience: Experience, ) { - // Generate a unique ID for the item - // This is used to identify the item in the tree, not the item itself - // The item id is not guaranteed to be unique - this.id = uuid(); - this.contextValue = `${experience.api}.item.item`; + const uniqueId = this.generateUniqueId(this.model); + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/documents/${uniqueId}`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } getTreeItem(): TreeItem { @@ -60,27 +62,49 @@ export abstract class DocumentDBItemResourceItem implements CosmosDBTreeElement if (!this.model.container.partitionKey || this.model.container.partitionKey.paths.length === 0) { return ''; } + const partitionKeyPaths = this.model.container.partitionKey.paths.join(', '); - let partitionKeyValues = extractPartitionKey(this.model.item, this.model.container.partitionKey); - partitionKeyValues = Array.isArray(partitionKeyValues) ? partitionKeyValues : [partitionKeyValues]; - partitionKeyValues = partitionKeyValues.map((v) => { - if (v === null) { - return '\\'; - } - if (v === undefined) { - return '\\'; - } - if (typeof v === 'object') { - return JSON.stringify(v); - } - return v; - }); + const partitionKeyValues = this.generatePartitionKeyValue(this.model); return ( '### Partition Key\n' + '---\n' + `- Paths: **${partitionKeyPaths}**\n` + - `- Values: **${partitionKeyValues.join(', ')}**\n` + `- Values: **${partitionKeyValues}**\n` ); } + + protected generateUniqueId(model: DocumentDBItemModel): string { + const documentId = getDocumentId(model.item, model.container.partitionKey); + const id = documentId?.id; + const rid = documentId?._rid; + const partitionKeyValues = this.generatePartitionKeyValue(model); + + return `${id || ''}|${partitionKeyValues || ''}|${rid || ''}`; + } + + protected generatePartitionKeyValue(model: DocumentDBItemModel): string { + if (!model.container.partitionKey || model.container.partitionKey.paths.length === 0) { + return ''; + } + + let partitionKeyValues = extractPartitionKey(model.item, model.container.partitionKey); + partitionKeyValues = Array.isArray(partitionKeyValues) ? partitionKeyValues : [partitionKeyValues]; + partitionKeyValues = partitionKeyValues + .map((v) => { + if (v === null) { + return '\\'; + } + if (v === undefined) { + return '\\'; + } + if (typeof v === 'object') { + return JSON.stringify(v); + } + return v; + }) + .join(', '); + + return partitionKeyValues; + } } diff --git a/src/tree/docdb/DocumentDBItemsResourceItem.ts b/src/tree/docdb/DocumentDBItemsResourceItem.ts index 89e085c9f..5a1dce80e 100644 --- a/src/tree/docdb/DocumentDBItemsResourceItem.ts +++ b/src/tree/docdb/DocumentDBItemsResourceItem.ts @@ -4,58 +4,51 @@ *--------------------------------------------------------------------------------------------*/ import { type CosmosClient, type FeedOptions, type ItemDefinition, type QueryIterator } from '@azure/cosmos'; -import { - callWithTelemetryAndErrorHandling, - createGenericElement, - type IActionContext, -} from '@microsoft/vscode-azext-utils'; -import { v4 as uuid } from 'uuid'; +import { createContextValue, createGenericElement, type IActionContext } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { getCosmosClient } from '../../docdb/getCosmosClient'; import { getBatchSizeSetting } from '../../utils/workspacUtils'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBItemsModel } from './models/DocumentDBItemsModel'; -export abstract class DocumentDBItemsResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.items'; +export abstract class DocumentDBItemsResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.documents'; protected iterator: QueryIterator | undefined; protected cachedItems: ItemDefinition[] = []; protected hasMoreChildren: boolean = true; protected constructor( - protected readonly model: DocumentDBItemsModel, - protected readonly experience: Experience, + public readonly model: DocumentDBItemsModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.items`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/documents`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } public async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; + if (this.iterator && this.cachedItems.length > 0) { + // ignore + } else { + // Fetch the first batch + const batchSize = getBatchSizeSetting(); + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); - if (this.iterator && this.cachedItems.length > 0) { - // ignore - } else { - // Fetch the first batch - const batchSize = getBatchSizeSetting(); - const { endpoint, credentials, isEmulator } = this.model.accountInfo; - const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + this.iterator = this.getIterator(cosmosClient, { maxItemCount: batchSize }); - this.iterator = this.getIterator(cosmosClient, { maxItemCount: batchSize }); - - await this.getItems(this.iterator); - } + await this.getItems(this.iterator); + } - return await this.getChildrenImpl(this.cachedItems); - }); + const result = await this.getChildrenImpl(this.cachedItems); - if (result && this.hasMoreChildren) { + if (this.hasMoreChildren) { result.push( createGenericElement({ contextValue: this.contextValue, @@ -81,7 +74,7 @@ export abstract class DocumentDBItemsResourceItem implements CosmosDBTreeElement ); } - return result ?? []; + return result; } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts b/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts index 67dcfacd5..8ec5890b8 100644 --- a/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts +++ b/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts @@ -3,22 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBStoredProcedureModel } from './models/DocumentDBStoredProcedureModel'; -export abstract class DocumentDBStoredProcedureResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.storedProcedure'; +export abstract class DocumentDBStoredProcedureResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.storedProcedure'; protected constructor( - protected readonly model: DocumentDBStoredProcedureModel, - protected readonly experience: Experience, + public readonly model: DocumentDBStoredProcedureModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.storedProcedure`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/storedProcedures/${model.procedure.id}`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts b/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts index 313494a47..36d7d93c8 100644 --- a/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts +++ b/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts @@ -4,40 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import { type CosmosClient, type Resource, type StoredProcedureDefinition } from '@azure/cosmos'; -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { getCosmosClient } from '../../docdb/getCosmosClient'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBStoredProceduresModel } from './models/DocumentDBStoredProceduresModel'; -export abstract class DocumentDBStoredProceduresResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.storedProcedures'; +export abstract class DocumentDBStoredProceduresResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.storedProcedures'; protected constructor( - protected readonly model: DocumentDBStoredProceduresModel, - protected readonly experience: Experience, + public readonly model: DocumentDBStoredProceduresModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.storedProcedures`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/storedProcedures`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } public async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + const storedProcedures = await this.getStoredProcedures(cosmosClient); - const { endpoint, credentials, isEmulator } = this.model.accountInfo; - const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); - const storedProcedures = await this.getStoredProcedures(cosmosClient); - - return await this.getChildrenImpl(storedProcedures); - }); - - return result ?? []; + return this.getChildrenImpl(storedProcedures); } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBTriggerResourceItem.ts b/src/tree/docdb/DocumentDBTriggerResourceItem.ts index e7f885418..1a3acd125 100644 --- a/src/tree/docdb/DocumentDBTriggerResourceItem.ts +++ b/src/tree/docdb/DocumentDBTriggerResourceItem.ts @@ -3,22 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBTriggerModel } from './models/DocumentDBTriggerModel'; -export abstract class DocumentDBTriggerResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.trigger'; +export abstract class DocumentDBTriggerResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.trigger'; protected constructor( - protected readonly model: DocumentDBTriggerModel, - protected readonly experience: Experience, + public readonly model: DocumentDBTriggerModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.trigger`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/triggers/${model.trigger.id}`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBTriggersResourceItem.ts b/src/tree/docdb/DocumentDBTriggersResourceItem.ts index 3adb78907..e3dcd3351 100644 --- a/src/tree/docdb/DocumentDBTriggersResourceItem.ts +++ b/src/tree/docdb/DocumentDBTriggersResourceItem.ts @@ -4,40 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import { type CosmosClient, type Resource, type TriggerDefinition } from '@azure/cosmos'; -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { getCosmosClient } from '../../docdb/getCosmosClient'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBTriggersModel } from './models/DocumentDBTriggersModel'; -export abstract class DocumentDBTriggersResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.triggers'; +export abstract class DocumentDBTriggersResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.triggers'; protected constructor( - protected readonly model: DocumentDBTriggersModel, - protected readonly experience: Experience, + public readonly model: DocumentDBTriggersModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.triggers`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/triggers`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } public async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + const triggers = await this.getTriggers(cosmosClient); - const { endpoint, credentials, isEmulator } = this.model.accountInfo; - const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); - const triggers = await this.getTriggers(cosmosClient); - - return await this.getChildrenImpl(triggers); - }); - - return result ?? []; + return this.getChildrenImpl(triggers); } getTreeItem(): TreeItem { diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index 19afb4112..b0a6d3134 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -28,14 +28,15 @@ import { type MongoAccountModel } from './MongoAccountModel'; export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { public declare readonly account: MongoAccountModel; + public readonly contextValue: string = 'treeItem.mongoCluster'; constructor( account: MongoAccountModel, - readonly experience: Experience, + experience: Experience, readonly databaseAccount?: DatabaseAccountGetResults, // TODO: exploring during v1->v2 migration readonly isEmulator?: boolean, // TODO: exploring during v1->v2 migration ) { - super(account); + super(account, experience); } async discoverConnectionString(): Promise { From 2518a39f4c3a33ba46a3141c12f122c6b675ddbe Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 15 Jan 2025 15:24:36 +0100 Subject: [PATCH 34/42] Updated 'copy connection string' command activation in package.json --- package.json | 8 +------- src/tree/mongo/MongoAccountResourceItem.ts | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 84323543d..1c538f142 100644 --- a/package.json +++ b/package.json @@ -843,13 +843,7 @@ "group": "2@1" }, { - "//": "[Account] Copy connection string to Mongo DB account", - "command": "cosmosDB.copyConnectionString", - "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", - "group": "2@1" - }, - { - "//": "[Account] Copy connection string to Mongo Cluster account", + "//": "[Account] Copy connection string to Mongo Cluster or MongoDB (RU) account", "command": "command.mongoClusters.copyConnectionString", "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", "group": "2@1" diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index b0a6d3134..48e4e2eb0 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -28,7 +28,7 @@ import { type MongoAccountModel } from './MongoAccountModel'; export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { public declare readonly account: MongoAccountModel; - public readonly contextValue: string = 'treeItem.mongoCluster'; + public readonly contextValue: string = 'treeItem.mongoCluster'; // TODO: this is a bug and overwrites the contextValue from the base class, fix this. constructor( account: MongoAccountModel, From ff895fea44f0d2130a2c7b90bb09e4b9aa708dbd Mon Sep 17 00:00:00 2001 From: Dmitry Shilov Date: Wed, 15 Jan 2025 15:27:10 +0100 Subject: [PATCH 35/42] Dev/sda/tree api migration (#2533) Syncing with @bk201- work --- extension.bundle.ts | 17 +- package.json | 525 ++++++++---------- .../deleteCosmosDBAccount.ts | 9 +- .../deleteDatabaseAccount.ts | 13 +- src/extensionVariables.ts | 17 +- src/mongoClusters/tree/CollectionItem.ts | 22 +- src/mongoClusters/tree/DatabaseItem.ts | 23 +- src/mongoClusters/tree/IndexItem.ts | 16 +- src/mongoClusters/tree/IndexesItem.ts | 16 +- .../tree/MongoClusterItemBase.ts | 25 +- .../workspace/MongoClusterWorkspaceItem.ts | 3 +- .../workspace/MongoDBAccountsWorkspaceItem.ts | 2 +- src/tree/AttachedAccountsTreeItem.ts | 7 +- src/tree/AzureAccountTreeItemWithAttached.ts | 36 -- src/tree/CosmosAccountResourceItemBase.ts | 39 +- src/tree/CosmosDBBranchDataProvider.ts | 24 +- .../CosmosDBWorkspaceBranchDataProvider.ts | 23 +- src/tree/TreeElementWithContextValue.ts | 12 + .../CosmosDBAttachedAccountsResourceItem.ts | 91 ++- .../DocumentDBAccountAttachedResourceItem.ts | 38 +- .../docdb/DocumentDBAccountResourceItem.ts | 49 +- .../docdb/DocumentDBContainerResourceItem.ts | 37 +- .../docdb/DocumentDBDatabaseResourceItem.ts | 37 +- src/tree/docdb/DocumentDBItemResourceItem.ts | 76 ++- src/tree/docdb/DocumentDBItemsResourceItem.ts | 57 +- .../DocumentDBStoredProcedureResourceItem.ts | 20 +- .../DocumentDBStoredProceduresResourceItem.ts | 37 +- .../docdb/DocumentDBTriggerResourceItem.ts | 20 +- .../docdb/DocumentDBTriggersResourceItem.ts | 37 +- src/tree/mongo/MongoAccountResourceItem.ts | 7 +- .../table/TableAccountAttachedResourceItem.ts | 4 +- src/tree/table/TableAccountResourceItem.ts | 4 +- 32 files changed, 675 insertions(+), 668 deletions(-) delete mode 100644 src/tree/AzureAccountTreeItemWithAttached.ts create mode 100644 src/tree/TreeElementWithContextValue.ts diff --git a/extension.bundle.ts b/extension.bundle.ts index fdb1a8a29..3bb2aefcf 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -24,19 +24,28 @@ export { emulatorPassword, isWindows } from './src/constants'; export { ParsedDocDBConnectionString, parseDocDBConnectionString } from './src/docdb/docDBConnectionStrings'; export { getCosmosClient } from './src/docdb/getCosmosClient'; export * from './src/docdb/registerDocDBCommands'; -export { activateInternal, cosmosDBCopyConnectionString, createServer, deactivateInternal, deleteAccount } from './src/extension'; +export { + activateInternal, + cosmosDBCopyConnectionString, + createServer, + deactivateInternal, + deleteAccount, +} from './src/extension'; export { ext } from './src/extensionVariables'; export * from './src/graph/registerGraphCommands'; +export { connectToMongoClient, isCosmosEmulatorConnectionString } from './src/mongo/connectToMongoClient'; export { MongoCommand } from './src/mongo/MongoCommand'; +export { + addDatabaseToAccountConnectionString, + encodeMongoConnectionString, + getDatabaseNameFromConnectionString, +} from './src/mongo/mongoConnectionStrings'; export { findCommandAtPosition, getAllCommandsFromText } from './src/mongo/MongoScrapbook'; export { MongoShell } from './src/mongo/MongoShell'; -export { connectToMongoClient, isCosmosEmulatorConnectionString } from './src/mongo/connectToMongoClient'; -export { addDatabaseToAccountConnectionString, encodeMongoConnectionString, getDatabaseNameFromConnectionString } from './src/mongo/mongoConnectionStrings'; export * from './src/mongo/registerMongoCommands'; export { IDatabaseInfo } from './src/mongo/tree/MongoAccountTreeItem'; export { addDatabaseToConnectionString } from './src/postgres/postgresConnectionStrings'; export { AttachedAccountsTreeItem, MONGO_CONNECTION_EXPECTED } from './src/tree/AttachedAccountsTreeItem'; -export { AzureAccountTreeItemWithAttached } from './src/tree/AzureAccountTreeItemWithAttached'; export * from './src/utils/azureClients'; export { getPublicIpv4, isIpInRanges } from './src/utils/getIp'; export { improveError } from './src/utils/improveError'; diff --git a/package.json b/package.json index e5617eb72..1c538f142 100644 --- a/package.json +++ b/package.json @@ -701,474 +701,412 @@ "when": "view == azureResourceGroups && viewItem =~ /(AzureCosmosDb|PostgreSqlServers(Standard|Flexible))/i && viewItem =~ /azureResourceTypeGroup/i", "group": "1@1" }, + { - "command": "cosmosDB.deleteAccount", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /mongodb.item.account(?![a-z])/i", + "command": "azureDatabases.detachDatabaseAccount", + "when": "view == azureWorkspace && viewItem == postgresServerAttached", "group": "1@2" }, { - "command": "cosmosDB.deleteAccount", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /mongoclusters/i && viewItem =~ /treeitem.mongoCluster/i", + "command": "postgreSQL.deleteServer", + "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /postgresServer(?![a-z])/i", "group": "1@2" }, { - "command": "cosmosDB.deleteAccount", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBDocumentServer(?![a-z])/i", - "group": "1@2" + "command": "postgreSQL.showPasswordlessWiki", + "when": "view =~ /azure(ResourceGroups|azureFocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i && viewItem =~ /usesPassword/i", + "group": "inline" }, { - "command": "cosmosDB.deleteAccount", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBGraphAccount(?![a-z])/i", - "group": "1@2" + "command": "postgreSQL.createDatabase", + "when": "view =~ /(azureResourceGroups|Workspace|azureFocusView)/ && viewItem =~ /postgresServer/i", + "group": "1@1" }, { - "command": "cosmosDB.deleteAccount", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBTableAccount(?![a-z])/i", + "command": "postgreSQL.deleteDatabase", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", "group": "1@2" }, { - "command": "postgreSQL.deleteServer", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /postgresServer(?![a-z])/i", + "command": "postgreSQL.deleteTable", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresTable", "group": "1@2" }, { - "command": "cosmosDB.createDocDBDocument", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentsGroup", + "command": "postgreSQL.deleteFunction", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresFunction", "group": "1@2" }, { - "command": "cosmosDB.openNoSqlQueryEditor", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection && config.cosmosDB.preview.queryEditor", - "group": "1@1" - }, - { - "command": "cosmosDB.openNoSqlQueryEditor", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentsGroup && config.cosmosDB.preview.queryEditor", - "group": "1@1" - }, - { - "command": "cosmosDB.writeNoSqlQuery", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", + "command": "postgreSQL.deleteStoredProcedure", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresStoredProcedure", "group": "1@2" }, { - "command": "cosmosDB.importDocument", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", - "group": "1@3" + "command": "postgreSQL.copyConnectionString", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", + "group": "2@1" }, { - "command": "cosmosDB.createDocDBStoredProcedure", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBStoredProceduresGroup", + "command": "postgreSQL.connectDatabase", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", "group": "1@1" }, { - "command": "cosmosDB.createDocDBTrigger", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBTriggersGroup", + "command": "postgreSQL.createFunctionQuery", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresFunctions", "group": "1@1" }, { - "command": "cosmosDB.createDocDBCollection", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentDatabase", + "command": "postgreSQL.createStoredProcedureQuery", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresStoredProcedures", "group": "1@1" }, { - "command": "cosmosDB.createDocDBDatabase", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBDocumentServer(?![a-z])/i", - "group": "1@1" + "command": "azureDatabases.refresh", + "when": "view =~ /azureWorkspace/ && viewItem =~ /postgresServer(?![a-z])/i", + "group": "2@2" }, { - "command": "cosmosDB.createDocDBDatabase", - "when": "view == azureWorkspace && viewItem == cosmosDBDocumentServerAttached", - "group": "1@1" + "command": "azureDatabases.refresh", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", + "group": "3@1" }, { - "command": "cosmosDB.createGraphDatabase", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBGraphAccount(?![a-z])/i", + "command": "azureDatabases.refresh", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresTables", "group": "1@1" }, { - "command": "cosmosDB.createGraphDatabase", - "when": "view == azureWorkspace && viewItem == cosmosDBGraphAccountAttached", - "group": "1@1" + "command": "azureDatabases.refresh", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresFunctions", + "group": "2@1" }, { - "command": "cosmosDB.createGraph", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBGraphDatabase", - "group": "1@1" + "command": "azureDatabases.refresh", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresStoredProcedures", + "group": "2@1" }, { - "command": "postgreSQL.showPasswordlessWiki", - "when": "view =~ /azure(ResourceGroups|azureFocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i && viewItem =~ /usesPassword/i", - "group": "inline" + "command": "azureDatabases.refresh", + "when": "view == azureWorkspace && viewItem == postgresServerAttached", + "group": "2@1" }, + + { - "command": "postgreSQL.createDatabase", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /postgresServer(?![a-z])/i", + "//": "[Account] Create Cosmos DB database", + "command": "cosmosDB.createDocDBDatabase", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]account(?![a-z.\\/])/i", "group": "1@1" }, { - "command": "postgreSQL.createDatabase", - "when": "view == azureWorkspace && viewItem == postgresServerAttached", + "//": "[Account] Create Mongo DB|Cluster database", + "command": "command.mongoClusters.createDatabase", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", "group": "1@1" }, { - "command": "azureDatabases.detachDatabaseAccount", - "when": "view == azureWorkspace && viewItem == cosmosDBMongoServerAttached", + "//": "[Account] Delete Cosmos DB account", + "command": "cosmosDB.deleteAccount", + "when": "view =~ /azure(ResourceGroups|FocusView)/ && viewItem =~ /treeitem[.]account(?![a-z.\\/])/i", "group": "1@2" }, { - "command": "azureDatabases.detachDatabaseAccount", - "when": "view == azureWorkspace && viewItem == cosmosDBGraphAccountAttached", + "//": "[Account] Delete Mongo DB account", + "command": "cosmosDB.deleteAccount", + "when": "view =~ /azure(ResourceGroups|FocusView)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", "group": "1@2" }, { + "//": "[Account] Detach Cosmos DB account (workspace only)", "command": "azureDatabases.detachDatabaseAccount", - "when": "view == azureWorkspace && viewItem == cosmosDBDocumentServerAttached", + "when": "view =~ /azureWorkspace/ && viewItem =~ /treeitem[.]account(?![a-z.\\/])/i", "group": "1@2" }, { + "//": "[Account] Detach Mongo DB account (workspace only)", "command": "azureDatabases.detachDatabaseAccount", - "when": "view == azureWorkspace && viewItem == cosmosDBTableAccountAttached", + "when": "view =~ /azureWorkspace/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", "group": "1@2" }, { - "command": "azureDatabases.detachDatabaseAccount", - "when": "view == azureWorkspace && viewItem == postgresServerAttached", + "//": "[Account] Detach Mongo Cluster account (workspace only)", + "command": "command.mongoClusters.removeWorkspaceConnection", + "when": "view =~ /azureWorkspace/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", "group": "1@2" }, { - "command": "cosmosDB.connectMongoDB", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == mongoDb", + "//": "[Account] Copy connection string to Cosmos DB account", + "command": "cosmosDB.copyConnectionString", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]account(?![a-z.\\/])/i", "group": "2@1" }, { - "command": "cosmosDB.deleteDocDBCollection", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", - "group": "1@4" - }, - { - "command": "cosmosDB.viewDocDBCollectionOffer", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", - "group": "1@5" - }, - { - "command": "cosmosDB.viewDocDBDatabaseOffer", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentDatabase", - "group": "1@5" + "//": "[Account] Copy connection string to Mongo Cluster or MongoDB (RU) account", + "command": "command.mongoClusters.copyConnectionString", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", + "group": "2@1" }, { - "command": "cosmosDB.deleteDocDBDocument", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocument", - "group": "1@2" + "//": "[Account] Mongo DB|Cluster Launch Shell", + "command": "command.mongoClusters.launchShell", + "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", + "group": "2@2" }, { - "command": "cosmosDB.deleteDocDBStoredProcedure", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBStoredProcedure", - "group": "1@2" + "//": "[Account] Refresh", + "command": "azureDatabases.refresh", + "when": "view =~ /azureWorkspace/ && viewItem =~ /treeitem[.](mongoCluster|account)(?![a-z.\\/])/i", + "group": "3@1" }, + + { - "command": "cosmosDB.executeDocDBStoredProcedure", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBStoredProcedure", + "//": "[Database] Create Graph container", + "command": "cosmosDB.createGraph", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i && viewItem =~ /experience[.](graph)/i", "group": "1@1" }, { - "command": "cosmosDB.deleteDocDBTrigger", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBTrigger", - "group": "1@2" + "//": "[Database] Create NoSql container", + "command": "cosmosDB.createDocDBCollection", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i && viewItem =~ /experience[.](table|cassandra|core)/i", + "group": "1@1" }, { - "command": "cosmosDB.deleteDocDBDatabase", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentDatabase", - "group": "1@2" + "//": "[Database] Create Mongo DB|Cluster collection", + "command": "command.mongoClusters.createCollection", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster|mongodb)/i", + "group": "1@1" }, { + "//": "[Database] Delete Graph database", "command": "cosmosDB.deleteGraphDatabase", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBGraphDatabase", - "group": "1@2" - }, - { - "command": "postgreSQL.deleteDatabase", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", - "group": "1@2" - }, - { - "command": "postgreSQL.deleteTable", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresTable", - "group": "1@2" - }, - { - "command": "postgreSQL.deleteFunction", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresFunction", - "group": "1@2" - }, - { - "command": "postgreSQL.deleteStoredProcedure", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresStoredProcedure", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i && viewItem =~ /experience[.](graph)/i", "group": "1@2" }, { - "command": "cosmosDB.deleteGraph", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBGraph", + "//": "[Database] Delete NoSql database", + "command": "cosmosDB.deleteDocDBDatabase", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i && viewItem =~ /experience[.](table|cassandra|core)/i", "group": "1@2" }, { - "command": "cosmosDB.attachDatabaseAccount", - "when": "view == azureWorkspace && viewItem =~ /cosmosDBAttachedAccounts(?![a-z])/gi", - "group": "1@1" - }, - { - "command": "cosmosDB.attachEmulator", - "when": "view == azureWorkspace && viewItem == cosmosDBAttachedAccountsWithEmulator", + "//": "[Database] Delete Mongo DB|Cluster database", + "command": "command.mongoClusters.dropDatabase", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster|mongodb)/i", "group": "1@2" }, { - "command": "cosmosDB.copyConnectionString", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBMongoServer(?![a-z])/i", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBGraphAccount(?![a-z])/i", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBDocumentServer(?![a-z])/i", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /cosmosDBTableAccount(?![a-z])/i", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view == azureWorkspace && viewItem == cosmosDBMongoServerAttached", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view == azureWorkspace && viewItem == cosmosDBGraphAccountAttached", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view == azureWorkspace && viewItem == cosmosDBDocumentServerAttached", - "group": "2@1" - }, - { - "command": "cosmosDB.copyConnectionString", - "when": "view == azureWorkspace && viewItem == cosmosDBTableAccountAttached", - "group": "2@1" - }, - { - "command": "postgreSQL.copyConnectionString", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", - "group": "2@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view == azureWorkspace && viewItem == cosmosDBMongoServerAttached", - "group": "3@2" + "//": "[Database] View NoSql database offer", + "command": "cosmosDB.viewDocDBDatabaseOffer", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i && viewItem =~ /experience[.](table|cassandra|core)/i", + "group": "1@3" }, { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection", + "//": "[Database] Connect to Mongo DB", + "command": "cosmosDB.connectMongoDB", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i && viewItem =~ /experience[.](mongodb)/i", "group": "2@1" }, { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentDatabase", - "group": "4@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentsGroup", + "//": "[Database] Mongo Cluster Launch Shell", + "command": "command.mongoClusters.launchShell", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster)/i", "group": "2@1" }, { + "//": "[Database] Refresh", "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBStoredProceduresGroup", - "group": "2@1" + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]database(?![a-z.\\/])/i", + "group": "3@1" }, + + { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBTriggersGroup", - "group": "2@1" + "//": "[Container] Open NoSql query editor", + "command": "cosmosDB.openNoSqlQueryEditor", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]container(?![a-z.\\/])/i && viewItem =~ /experience[.](table|cassandra|core)/i", + "group": "1@1" }, { - "command": "azureDatabases.refresh", - "when": "view =~ /azureWorkspace/ && viewItem =~ /cosmosDBDocumentServer(?![a-z])/i", - "group": "3@2" + "//": "[Container] Open NoSql query editor (scrapbook)", + "command": "cosmosDB.writeNoSqlQuery", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]container(?![a-z.\\/])/i && viewItem =~ /experience[.](table|cassandra|core)/i", + "group": "1@2" }, { - "command": "azureDatabases.refresh", - "when": "view =~ /azureWorkspace/ && viewItem =~ /postgresServer(?![a-z])/i", - "group": "2@2" + "//": "[Container] Import NoSql documents", + "command": "cosmosDB.importDocument", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]container(?![a-z.\\/])/i && viewItem =~ /experience[.](table|cassandra|core)/i", + "group": "1@3" }, { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", - "group": "3@1" + "//": "[Container] Delete NoSql container", + "command": "cosmosDB.deleteDocDBCollection", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]container(?![a-z.\\/])/i && viewItem =~ /experience[.](table|cassandra|core)/i", + "group": "1@4" }, { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresTables", - "group": "1@1" + "//": "[Container] Delete Graph container", + "command": "cosmosDB.deleteGraph", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]container(?![a-z.\\/])/i && viewItem =~ /experience[.](graph)/i", + "group": "1@2" }, { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresFunctions", - "group": "2@1" + "//": "[Container] View NoSql container offer", + "command": "cosmosDB.viewDocDBCollectionOffer", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]container(?![a-z.\\/])/i && viewItem =~ /experience[.](table|cassandra|core)/i", + "group": "1@5" }, { + "//": "[Container] Refresh", "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresStoredProcedures", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]container(?![a-z.\\/])/i", "group": "2@1" }, + + { - "command": "azureDatabases.refresh", - "when": "view == azureWorkspace && viewItem == cosmosDBDocumentServerAttached", - "group": "3@1" - }, - { - "command": "azureDatabases.refresh", - "when": "view =~ /azureWorkspace/ && viewItem =~ /cosmosDBGraphAccount(?![a-z])/i", - "group": "3@2" + "//": "[Collection] Open Mongo DB|Cluster collection", + "command": "command.mongoClusters.containerView.open", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]collection(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster|mongodb)/i", + "group": "1@1" }, { - "command": "azureDatabases.refresh", - "when": "view == azureWorkspace && viewItem == cosmosDBGraphAccountAttached", - "group": "3@1" + "//": "[Collection] Create Mongo DB|Cluster document", + "command": "command.mongoClusters.createDocument", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]collection(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster|mongodb)/i", + "group": "1@2" }, { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBGraphDatabase", + "//": "[Collection] Import Mongo DB|Cluster documents", + "command": "command.mongoClusters.importDocuments", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]collection(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster|mongodb)/i", "group": "2@1" }, { - "command": "azureDatabases.refresh", - "when": "view == azureWorkspace && viewItem == postgresServerAttached", - "group": "2@1" + "//": "[Collection] Export Mongo DB|Cluster documents", + "command": "command.mongoClusters.exportDocuments", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]collection(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster|mongodb)/i", + "group": "2@2" }, { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == mongoDb", + "//": "[Collection] Drop Mongo DB|Cluster collection", + "command": "command.mongoClusters.dropCollection", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]collection(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster|mongodb)/i", "group": "3@1" }, { - "command": "azureDatabases.refresh", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == MongoCollection", + "//": "[Collection] Launch Mongo DB|Cluster shell", + "command": "command.mongoClusters.launchShell", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]collection(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster|mongodb)/i", "group": "4@1" }, { + "//": "[Collection] Refresh", "command": "azureDatabases.refresh", - "when": "view == azureWorkspace && viewItem =~ /^cosmosDBAttachedAccounts(?![a-z])/gi", - "group": "2@1" - }, - { - "command": "cosmosDB.importDocument", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == MongoCollection", - "group": "1@3" - }, - { - "command": "postgreSQL.connectDatabase", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /postgresDatabase(?![a-z])/i", - "group": "1@1" - }, - { - "command": "postgreSQL.createFunctionQuery", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresFunctions", - "group": "1@1" + "when" : "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]collection(?![a-z.\\/])/i", + "group": "5@1" }, + + { - "command": "postgreSQL.createStoredProcedureQuery", - "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == postgresStoredProcedures", + "//": "[Stored Procedures] Create Stored Procedure", + "command": "cosmosDB.createDocDBStoredProcedure", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]storedProcedures(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", "group": "1@1" }, { - "command": "command.mongoClusters.dropCollection", - "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "3@1" + "//": "[Stored Procedures] Refresh", + "command": "azureDatabases.refresh", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]storedProcedures(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", + "group": "2@1" }, + + { - "command": "command.mongoClusters.dropDatabase", - "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", + "//": "[Stored Procedure] Execute", + "command": "cosmosDB.executeDocDBStoredProcedure", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]storedProcedure(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", "group": "1@1" }, { - "command": "command.mongoClusters.removeWorkspaceConnection", - "when": "vscodeDatabases.mongoClustersSupportEnabled && view == azureWorkspace && viewItem =~ /treeitem.mongoCluster/i && viewItem =~ /(mongocluster|mongodb)/i" - }, - { - "command": "command.mongoClusters.createCollection", - "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "1@1" + "//": "[Stored Procedure] Delete", + "command": "cosmosDB.deleteDocDBStoredProcedure", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]storedProcedure(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", + "group": "1@2" }, + + { - "command": "command.mongoClusters.createDatabase", - "when": "viewItem =~ /treeitem.mongoCluster|mongodb.item.account/i", + "//": "[Triggers] Create Trigger", + "command": "cosmosDB.createDocDBTrigger", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]triggers(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", "group": "1@1" }, { - "command": "command.mongoClusters.copyConnectionString", - "when": "viewItem =~ /mongodb.item.account/i || viewItem =~ /treeitem.mongoCluster/i && viewItem =~ /(mongocluster)/i", - "group": "2@1" - }, - { - "command": "command.mongoClusters.importDocuments", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", + "//": "[Triggers] Refresh", + "command": "azureDatabases.refresh", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]triggers(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", "group": "2@1" }, + + { - "command": "command.mongoClusters.exportDocuments", - "when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "2@2" + "//": "[Trigger] Delete", + "command": "cosmosDB.deleteDocDBTrigger", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]trigger(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", + "group": "1@1" }, + + { - "command": "command.mongoClusters.containerView.open", - "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", + "//": "[Documents] Open NoSql query editor", + "command": "cosmosDB.openNoSqlQueryEditor", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]documents(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", "group": "1@1" }, { - "command": "command.mongoClusters.createDocument", - "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", + "//": "[Documents] Create NoSql Document", + "command": "cosmosDB.createDocDBDocument", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]documents(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", "group": "1@2" }, { - "command": "command.mongoClusters.launchShell", - "when": "viewItem =~ /treeitem.mongoCluster|mongodb.item.account/i", - "group": "2@1" - }, - { - "command": "command.mongoClusters.launchShell", - "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", + "//": "[Documents] Refresh", + "command": "azureDatabases.refresh", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]documents(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", "group": "2@1" }, + + { - "command": "command.mongoClusters.launchShell", - "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "4@1" - }, - { - "command": "azureDatabases.refresh", - "when": "viewItem =~ /treeitem.collection/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "5@1" + "//": "[Document] Delete", + "command": "cosmosDB.deleteDocDBDocument", + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]document(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", + "group": "1@1" }, { + "//": "[Document] Refresh", "command": "azureDatabases.refresh", - "when": "viewItem =~ /treeitem.database/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "3@1" + "when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]document(?![a-z.\\/])/i && viewItem =~ /experience[.](graph|table|cassandra|core)/i", + "group": "2@1" }, + + { + "//": "[Indexes] Refresh", "command": "azureDatabases.refresh", - "when": "viewItem =~ /treeitem.indexes/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "4@1" + "when" : "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]indexes(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster|mongodb)/i", + "group": "1@1" }, { + "//": "[Index] Refresh", "command": "azureDatabases.refresh", - "when": "viewItem =~ /treeitem.index/i && viewItem =~ /(mongocluster|mongodb)/i", - "group": "4@1" + "when" : "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem =~ /treeitem[.]index(?![a-z.\\/])/i && viewItem =~ /experience[.](mongocluster|mongodb)/i", + "group": "1@1" } ], "explorer/context": [ @@ -1325,11 +1263,6 @@ "type": "boolean", "default": true, "description": "Show warning dialog when uploading a document to the cloud." - }, - "cosmosDB.preview.queryEditor": { - "type": "boolean", - "default": true, - "description": "Enable the NoSQL Query Editor." } } } diff --git a/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts b/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts index ccb1a4848..ccd888e22 100644 --- a/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts @@ -6,6 +6,7 @@ import { type CosmosDBManagementClient } from '@azure/arm-cosmosdb'; import { getResourceGroupFromId } from '@microsoft/vscode-azext-azureutils'; import { AzExtTreeItem, createSubscriptionContext } from '@microsoft/vscode-azext-utils'; +import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; import { ext } from '../../extensionVariables'; import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; @@ -27,7 +28,13 @@ export async function deleteCosmosDBAccount( resourceGroup = getResourceGroupFromId(node.fullId); accountName = getDatabaseAccountNameFromId(node.fullId); } else if (node instanceof CosmosAccountResourceItemBase) { - const subscriptionContext = createSubscriptionContext(node.account.subscription); + // Not all CosmosAccountResourceItemBase instances have a subscription property (attached account does not), + // so we need to create a subscription context + if (!('subscription' in node.account)) { + throw new Error('Subscription is required to delete an account.'); + } + + const subscriptionContext = createSubscriptionContext(node.account.subscription as AzureSubscription); client = await createCosmosDBClient([context, subscriptionContext]); resourceGroup = getResourceGroupFromId(node.account.id); accountName = node.account.name; diff --git a/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts b/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts index 6723fe493..c4f68f5bc 100644 --- a/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts @@ -11,7 +11,8 @@ import { type IActionContext, type ISubscriptionContext, } from '@microsoft/vscode-azext-utils'; -import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; +import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; +import { MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; import { createActivityContext } from '../../utils/activityUtils'; import { localize } from '../../utils/localize'; @@ -26,10 +27,14 @@ export async function deleteDatabaseAccount( let subscription: ISubscriptionContext; if (node instanceof AzExtTreeItem) { subscription = node.subscription; - } else if (node instanceof CosmosAccountResourceItemBase) { - subscription = createSubscriptionContext(node.account.subscription); + } else if (node instanceof CosmosAccountResourceItemBase && 'subscription' in node.account) { + subscription = createSubscriptionContext(node.account.subscription as AzureSubscription); + } else if (node instanceof MongoClusterResourceItem) { + subscription = createSubscriptionContext(node.subscription); } else { - subscription = createSubscriptionContext((node as MongoClusterResourceItem).subscription); + // Not all CosmosAccountResourceItemBase instances have a subscription property (attached account does not), + // so we need to create a subscription context + throw new Error('Subscription is required to delete an account.'); } let accountName: string; diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 9cce4a590..57f6b57ae 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -3,15 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { - type AzExtTreeDataProvider, - type AzExtTreeItem, - type IAzExtLogOutputChannel, - type TreeElementStateManager, -} from '@microsoft/vscode-azext-utils'; +import { type IAzExtLogOutputChannel, type TreeElementStateManager } from '@microsoft/vscode-azext-utils'; import { type AzureHostExtensionApi } from '@microsoft/vscode-azext-utils/hostapi'; import { type AzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api'; -import { type ExtensionContext, type SecretStorage, type TreeView } from 'vscode'; +import { type ExtensionContext, type SecretStorage } from 'vscode'; import { type DatabasesFileSystem } from './DatabasesFileSystem'; import { type NoSqlCodeLensProvider } from './docdb/NoSqlCodeLensProvider'; import { type MongoDBLanguageClient } from './mongo/languageClient'; @@ -22,8 +17,6 @@ import { type MongoClustersWorkspaceBranchDataProvider } from './mongoClusters/t import { type PostgresCodeLensProvider } from './postgres/services/PostgresCodeLensProvider'; import { type PostgresDatabaseTreeItem } from './postgres/tree/PostgresDatabaseTreeItem'; import { type AttachedAccountsTreeItem } from './tree/AttachedAccountsTreeItem'; -import { type AzureAccountTreeItemWithAttached } from './tree/AzureAccountTreeItemWithAttached'; -import { type SharedWorkspaceResourceProvider } from './tree/workspace/SharedWorkspaceResourceProvider'; /** * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts @@ -33,11 +26,8 @@ export namespace ext { export let connectedPostgresDB: PostgresDatabaseTreeItem | undefined; export let context: ExtensionContext; export let outputChannel: IAzExtLogOutputChannel; - export let tree: AzExtTreeDataProvider; - export let treeView: TreeView; export let attachedAccountsNode: AttachedAccountsTreeItem; export let isBundle: boolean | undefined; - export let azureAccountTreeItem: AzureAccountTreeItemWithAttached; export let secretStorage: SecretStorage; export let postgresCodeLensProvider: PostgresCodeLensProvider | undefined; export const prefix: string = 'azureDatabases'; @@ -53,9 +43,6 @@ export namespace ext { // used for the resources tree export let mongoClustersBranchDataProvider: MongoClustersBranchDataProvider; - // used for the workspace: this is the general provider - export let workspaceDataProvider: SharedWorkspaceResourceProvider; - // used for the workspace: these are the dedicated providers export let mongoClustersWorkspaceBranchDataProvider: MongoClustersWorkspaceBranchDataProvider; diff --git a/src/mongoClusters/tree/CollectionItem.ts b/src/mongoClusters/tree/CollectionItem.ts index 2da6162c0..bdca220d6 100644 --- a/src/mongoClusters/tree/CollectionItem.ts +++ b/src/mongoClusters/tree/CollectionItem.ts @@ -11,22 +11,26 @@ import { type TreeElementWithId, } from '@microsoft/vscode-azext-utils'; import { type Document } from 'bson'; -import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type Experience } from '../../AzureDBExperiences'; +import { ThemeIcon, type TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { API, type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; +import { type TreeElementWithContextValue } from '../../tree/TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { - MongoClustersClient, type CollectionItemModel, type DatabaseItemModel, type InsertDocumentsResult, + MongoClustersClient, } from '../MongoClustersClient'; import { IndexesItem } from './IndexesItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class CollectionItem implements TreeElementWithId, TreeElementWithExperience { - id: string; - experience?: Experience; +export class CollectionItem implements TreeElementWithId, TreeElementWithExperience, TreeElementWithContextValue { + public readonly id: string; + public readonly experience?: Experience; + public readonly contextValue: string = 'treeItem.collection'; + + private readonly experienceContextValue: string = ''; constructor( readonly mongoCluster: MongoClusterModel, @@ -35,12 +39,14 @@ export class CollectionItem implements TreeElementWithId, TreeElementWithExperie ) { this.id = `${mongoCluster.id}/${databaseInfo.name}/${collectionInfo.name}`; this.experience = mongoCluster.dbExperience; + this.experienceContextValue = `experience.${this.experience?.api ?? API.Common}`; + this.contextValue = createContextValue([this.contextValue, this.experienceContextValue]); } async getChildren(): Promise { return [ createGenericElement({ - contextValue: createContextValue(['treeitem.documents', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: createContextValue(['treeItem.documents', this.experienceContextValue]), id: `${this.id}/documents`, label: 'Documents', commandId: 'command.internal.mongoClusters.containerView.open', @@ -90,7 +96,7 @@ export class CollectionItem implements TreeElementWithId, TreeElementWithExperie getTreeItem(): TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.collection', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: this.collectionInfo.name, iconPath: new ThemeIcon('folder-opened'), collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/DatabaseItem.ts b/src/mongoClusters/tree/DatabaseItem.ts index 25c456274..a66b80818 100644 --- a/src/mongoClusters/tree/DatabaseItem.ts +++ b/src/mongoClusters/tree/DatabaseItem.ts @@ -12,17 +12,21 @@ import { } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type Experience } from '../../AzureDBExperiences'; +import { API, type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; +import { type TreeElementWithContextValue } from '../../tree/TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { localize } from '../../utils/localize'; import { MongoClustersClient, type DatabaseItemModel } from '../MongoClustersClient'; import { CollectionItem } from './CollectionItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class DatabaseItem implements TreeElementWithId, TreeElementWithExperience { - id: string; - experience?: Experience; +export class DatabaseItem implements TreeElementWithId, TreeElementWithExperience, TreeElementWithContextValue { + public readonly id: string; + public readonly experience?: Experience; + public readonly contextValue: string = 'treeItem.database'; + + private readonly experienceContextValue: string = ''; constructor( readonly mongoCluster: MongoClusterModel, @@ -30,6 +34,8 @@ export class DatabaseItem implements TreeElementWithId, TreeElementWithExperienc ) { this.id = `${mongoCluster.id}/${databaseInfo.name}`; this.experience = mongoCluster.dbExperience; + this.experienceContextValue = `experience.${this.experience?.api ?? API.Common}`; + this.contextValue = createContextValue([this.contextValue, this.experienceContextValue]); } async getChildren(): Promise { @@ -40,11 +46,8 @@ export class DatabaseItem implements TreeElementWithId, TreeElementWithExperienc // no databases in there: return [ createGenericElement({ - contextValue: createContextValue([ - 'treeitem.no-collections', - this.mongoCluster.dbExperience?.api ?? '', - ]), - id: `${this.id}/no-databases`, + contextValue: createContextValue(['treeItem.no-collections', this.experienceContextValue]), + id: `${this.id}/no-collections`, label: 'Create collection...', iconPath: new vscode.ThemeIcon('plus'), commandId: 'command.mongoClusters.createCollection', @@ -94,7 +97,7 @@ export class DatabaseItem implements TreeElementWithId, TreeElementWithExperienc getTreeItem(): TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.database', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: this.databaseInfo.name, iconPath: new ThemeIcon('database'), // TODO: create our own icon here, this one's shape can change collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/IndexItem.ts b/src/mongoClusters/tree/IndexItem.ts index c15e3486e..d4bd544c2 100644 --- a/src/mongoClusters/tree/IndexItem.ts +++ b/src/mongoClusters/tree/IndexItem.ts @@ -10,14 +10,18 @@ import { type TreeElementWithId, } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type Experience } from '../../AzureDBExperiences'; +import { API, type Experience } from '../../AzureDBExperiences'; +import { type TreeElementWithContextValue } from '../../tree/TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { type CollectionItemModel, type DatabaseItemModel, type IndexItemModel } from '../MongoClustersClient'; import { type MongoClusterModel } from './MongoClusterModel'; -export class IndexItem implements TreeElementWithId, TreeElementWithExperience { - id: string; - experience?: Experience; +export class IndexItem implements TreeElementWithId, TreeElementWithExperience, TreeElementWithContextValue { + public readonly id: string; + public readonly experience?: Experience; + public readonly contextValue: string = 'treeItem.index'; + + private readonly experienceContextValue: string = ''; constructor( readonly mongoCluster: MongoClusterModel, @@ -27,6 +31,8 @@ export class IndexItem implements TreeElementWithId, TreeElementWithExperience { ) { this.id = `${mongoCluster.id}/${databaseInfo.name}/${collectionInfo.name}/indexes/${indexInfo.name}`; this.experience = mongoCluster.dbExperience; + this.experienceContextValue = `experience.${this.experience?.api ?? API.Common}`; + this.contextValue = createContextValue([this.contextValue, this.experienceContextValue]); } async getChildren(): Promise { @@ -47,7 +53,7 @@ export class IndexItem implements TreeElementWithId, TreeElementWithExperience { getTreeItem(): TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.index', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: this.indexInfo.name, iconPath: new ThemeIcon('combine'), // TODO: create our onw icon here, this one's shape can change collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/IndexesItem.ts b/src/mongoClusters/tree/IndexesItem.ts index efa7575b0..303bcaf60 100644 --- a/src/mongoClusters/tree/IndexesItem.ts +++ b/src/mongoClusters/tree/IndexesItem.ts @@ -5,15 +5,19 @@ import { createContextValue, type TreeElementBase, type TreeElementWithId } from '@microsoft/vscode-azext-utils'; import { ThemeIcon, TreeItemCollapsibleState, type TreeItem } from 'vscode'; -import { type Experience } from '../../AzureDBExperiences'; +import { API, type Experience } from '../../AzureDBExperiences'; +import { type TreeElementWithContextValue } from '../../tree/TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { MongoClustersClient, type CollectionItemModel, type DatabaseItemModel } from '../MongoClustersClient'; import { IndexItem } from './IndexItem'; import { type MongoClusterModel } from './MongoClusterModel'; -export class IndexesItem implements TreeElementWithId, TreeElementWithExperience { - id: string; - experience?: Experience; +export class IndexesItem implements TreeElementWithId, TreeElementWithExperience, TreeElementWithContextValue { + public readonly id: string; + public readonly experience?: Experience; + public readonly contextValue: string = 'treeItem.indexes'; + + private readonly experienceContextValue: string = ''; constructor( readonly mongoCluster: MongoClusterModel, @@ -22,6 +26,8 @@ export class IndexesItem implements TreeElementWithId, TreeElementWithExperience ) { this.id = `${mongoCluster.id}/${databaseInfo.name}/${collectionInfo.name}/indexes`; this.experience = mongoCluster.dbExperience; + this.experienceContextValue = `experience.${this.experience?.api ?? API.Common}`; + this.contextValue = createContextValue([this.contextValue, this.experienceContextValue]); } async getChildren(): Promise { @@ -35,7 +41,7 @@ export class IndexesItem implements TreeElementWithId, TreeElementWithExperience getTreeItem(): TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.indexes', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: 'Indexes', iconPath: new ThemeIcon('combine'), // TODO: create our onw icon here, this one's shape can change collapsibleState: TreeItemCollapsibleState.Collapsed, diff --git a/src/mongoClusters/tree/MongoClusterItemBase.ts b/src/mongoClusters/tree/MongoClusterItemBase.ts index 48eabc8d5..24e474b3f 100644 --- a/src/mongoClusters/tree/MongoClusterItemBase.ts +++ b/src/mongoClusters/tree/MongoClusterItemBase.ts @@ -13,8 +13,9 @@ import { import { type TreeItem } from 'vscode'; import * as vscode from 'vscode'; -import { type Experience } from '../../AzureDBExperiences'; +import { API, type Experience } from '../../AzureDBExperiences'; import { ext } from '../../extensionVariables'; +import { type TreeElementWithContextValue } from '../../tree/TreeElementWithContextValue'; import { type TreeElementWithExperience } from '../../tree/TreeElementWithExperience'; import { localize } from '../../utils/localize'; import { regionToDisplayName } from '../../utils/regionToDisplayName'; @@ -24,13 +25,20 @@ import { DatabaseItem } from './DatabaseItem'; import { type MongoClusterModel } from './MongoClusterModel'; // This info will be available at every level in the tree for immediate access -export abstract class MongoClusterItemBase implements TreeElementWithId, TreeElementWithExperience { - id: string; - experience?: Experience; +export abstract class MongoClusterItemBase + implements TreeElementWithId, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly experience?: Experience; + public readonly contextValue: string = 'treeItem.mongoCluster'; - constructor(public mongoCluster: MongoClusterModel) { + private readonly experienceContextValue: string = ''; + + protected constructor(public mongoCluster: MongoClusterModel) { this.id = mongoCluster.id ?? ''; this.experience = mongoCluster.dbExperience; + this.experienceContextValue = `experience.${this.experience?.api ?? API.Common}`; + this.contextValue = createContextValue([this.contextValue, this.experienceContextValue]); } /** @@ -97,10 +105,7 @@ export abstract class MongoClusterItemBase implements TreeElementWithId, TreeEle if (databases.length === 0) { return [ createGenericElement({ - contextValue: createContextValue([ - 'treeitem.no-databases', - this.mongoCluster.dbExperience?.api ?? '', - ]), + contextValue: createContextValue(['treeItem.no-databases', this.experienceContextValue]), id: `${this.id}/no-databases`, label: 'Create database...', iconPath: new vscode.ThemeIcon('plus'), @@ -149,7 +154,7 @@ export abstract class MongoClusterItemBase implements TreeElementWithId, TreeEle getTreeItem(): TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.mongocluster', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: this.mongoCluster.name, description: this.mongoCluster.sku !== undefined ? `(${this.mongoCluster.sku})` : false, // iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg'), // Uncomment if icon is available diff --git a/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts index a0287d507..840f83b90 100644 --- a/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts @@ -6,7 +6,6 @@ import { AzureWizard, callWithTelemetryAndErrorHandling, - createContextValue, nonNullProp, nonNullValue, UserCancelledError, @@ -159,7 +158,7 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase { getTreeItem(): vscode.TreeItem { return { id: this.id, - contextValue: createContextValue(['treeitem.mongocluster', this.mongoCluster.dbExperience?.api ?? '']), + contextValue: this.contextValue, label: this.mongoCluster.name, description: this.mongoCluster.sku !== undefined ? `(${this.mongoCluster.sku})` : false, iconPath: new vscode.ThemeIcon('server-environment'), // Uncomment if icon is available diff --git a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts index 8e10d2cb1..5697d0620 100644 --- a/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoDBAccountsWorkspaceItem.ts @@ -35,7 +35,7 @@ export class MongoDBAccountsWorkspaceItem implements TreeElementWithId, TreeElem return new MongoClusterWorkspaceItem(model); }), createGenericElement({ - contextValue: 'treeitem.newConnection', + contextValue: 'treeItem.newConnection', id: this.id + '/newConnection', label: 'New Connection...', iconPath: new ThemeIcon('plus'), diff --git a/src/tree/AttachedAccountsTreeItem.ts b/src/tree/AttachedAccountsTreeItem.ts index 894bee0bc..28c11a66e 100644 --- a/src/tree/AttachedAccountsTreeItem.ts +++ b/src/tree/AttachedAccountsTreeItem.ts @@ -49,7 +49,7 @@ const localMongoConnectionString: string = 'mongodb://127.0.0.1:27017'; export class AttachedAccountsTreeItem extends AzExtParentTreeItem { public static contextValue: string = 'cosmosDBAttachedAccounts' + (isWindows ? 'WithEmulator' : 'WithoutEmulator'); public readonly contextValue: string = AttachedAccountsTreeItem.contextValue; - public readonly label: string = 'Attached Database Accounts'; + public readonly label: string = 'Attached Database Accounts (Postgres)'; public childTypeLabel: string = 'Account'; public suppressMaskLabel = true; @@ -359,7 +359,10 @@ export class AttachedAccountsTreeItem extends AzExtParentTreeItem { await ext.secretStorage.get(getSecretStorageKey(this._serviceName, id)), 'connectionString', ); - persistedAccounts.push(await this.createTreeItem(connectionString, api, label, id, isEmulator)); + // TODO: Left only Postgres, other types are moved to new tree api v2 + if (api === API.PostgresSingle || api === API.PostgresFlexible) { + persistedAccounts.push(await this.createTreeItem(connectionString, api, label, id, isEmulator)); + } }), ); } diff --git a/src/tree/AzureAccountTreeItemWithAttached.ts b/src/tree/AzureAccountTreeItemWithAttached.ts deleted file mode 100644 index 226a1a43b..000000000 --- a/src/tree/AzureAccountTreeItemWithAttached.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzureAccountTreeItemBase } from '@microsoft/vscode-azext-azureutils'; -import { type AzExtTreeItem, type IActionContext, type ISubscriptionContext } from '@microsoft/vscode-azext-utils'; -import { ext } from '../extensionVariables'; -import { AttachedAccountsTreeItem } from './AttachedAccountsTreeItem'; -import { SubscriptionTreeItem } from './SubscriptionTreeItem'; - -export class AzureAccountTreeItemWithAttached extends AzureAccountTreeItemBase { - public constructor(testAccount?: object) { - super(undefined, testAccount); - ext.attachedAccountsNode = new AttachedAccountsTreeItem(this); - } - - public createSubscriptionTreeItem(root: ISubscriptionContext): SubscriptionTreeItem { - return new SubscriptionTreeItem(this, root); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - const children: AzExtTreeItem[] = await super.loadMoreChildrenImpl(clearCache, context); - return children.concat(ext.attachedAccountsNode); - } - - public compareChildrenImpl(item1: AzExtTreeItem, item2: AzExtTreeItem): number { - if (item1 instanceof AttachedAccountsTreeItem) { - return 1; - } else if (item2 instanceof AttachedAccountsTreeItem) { - return -1; - } else { - return super.compareChildrenImpl(item1, item2); - } - } -} diff --git a/src/tree/CosmosAccountResourceItemBase.ts b/src/tree/CosmosAccountResourceItemBase.ts index a32bb2f29..e3975ccfa 100644 --- a/src/tree/CosmosAccountResourceItemBase.ts +++ b/src/tree/CosmosAccountResourceItemBase.ts @@ -3,18 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { createContextValue } from '@microsoft/vscode-azext-utils'; +import { type ResourceBase } from '@microsoft/vscode-azureresources-api'; +import { v4 as uuid } from 'uuid'; import * as vscode from 'vscode'; import { type TreeItem } from 'vscode'; -import { getExperienceLabel, tryGetExperience } from '../AzureDBExperiences'; -import { type CosmosAccountModel } from './CosmosAccountModel'; +import { type Experience } from '../AzureDBExperiences'; import { type CosmosDBTreeElement } from './CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from './TreeElementWithContextValue'; +import { type TreeElementWithExperience } from './TreeElementWithExperience'; -export abstract class CosmosAccountResourceItemBase implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.account'; +export abstract class CosmosAccountResourceItemBase + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.account'; - protected constructor(readonly account: CosmosAccountModel) { - this.id = account.id ?? ''; + protected constructor( + public readonly account: ResourceBase, + public readonly experience: Experience, + ) { + this.id = account.id ?? uuid(); + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } /** @@ -30,22 +40,11 @@ export abstract class CosmosAccountResourceItemBase implements CosmosDBTreeEleme * @returns The TreeItem object. */ getTreeItem(): TreeItem { - const experience = tryGetExperience(this.account); - if (!experience) { - const accountKindLabel = getExperienceLabel(this.account); - const label: string = this.account.name + (accountKindLabel ? ` (${accountKindLabel})` : ``); - return { - id: this.id, - contextValue: 'cosmosDB.item.account', - label: label, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - }; - } return { id: this.id, - contextValue: `${experience.api}.item.account`, + contextValue: this.contextValue, label: this.account.name, - description: `(${experience.shortName})`, + description: `(${this.experience.shortName})`, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }; } diff --git a/src/tree/CosmosDBBranchDataProvider.ts b/src/tree/CosmosDBBranchDataProvider.ts index 4b9b1cf05..ca1be4315 100644 --- a/src/tree/CosmosDBBranchDataProvider.ts +++ b/src/tree/CosmosDBBranchDataProvider.ts @@ -11,7 +11,7 @@ import { } from '@microsoft/vscode-azext-utils'; import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; -import { API, tryGetExperience } from '../AzureDBExperiences'; +import { API, CoreExperience, tryGetExperience } from '../AzureDBExperiences'; import { databaseAccountType } from '../constants'; import { ext } from '../extensionVariables'; import { localize } from '../utils/localize'; @@ -22,6 +22,8 @@ import { GraphAccountResourceItem } from './graph/GraphAccountResourceItem'; import { MongoAccountResourceItem } from './mongo/MongoAccountResourceItem'; import { NoSqlAccountResourceItem } from './nosql/NoSqlAccountResourceItem'; import { TableAccountResourceItem } from './table/TableAccountResourceItem'; +import { isTreeElementWithContextValue } from './TreeElementWithContextValue'; +import { isTreeElementWithExperience } from './TreeElementWithExperience'; export class CosmosDBBranchDataProvider extends vscode.Disposable @@ -49,7 +51,20 @@ export class CosmosDBBranchDataProvider context.errorHandling.rethrow = true; context.errorHandling.forceIncludeInReportIssueCommand = true; - return (await element.getChildren?.())?.map((child) => { + if (isTreeElementWithContextValue(element)) { + context.telemetry.properties.parentContext = element.contextValue; + } + + if (isTreeElementWithExperience(element)) { + context.telemetry.properties.experience = element.experience?.api ?? API.Common; + } + + // TODO: values to mask. New TreeElements do not have valueToMask field + // I assume this array should be filled after element.getChildren() call + // And these values should be masked in the context + + const children = (await element.getChildren?.()) ?? []; + return children.map((child) => { return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => this.refresh(child), ) as CosmosDBTreeElement; @@ -75,7 +90,7 @@ export class CosmosDBBranchDataProvider async getResourceItem(resource: CosmosDBResource): Promise { const resourceItem = await callWithTelemetryAndErrorHandling( 'CosmosDBBranchDataProvider.getResourceItem', - async (context: IActionContext) => { + (context: IActionContext) => { const id = nonNullProp(resource, 'id'); const name = nonNullProp(resource, 'name'); const type = nonNullProp(resource, 'type'); @@ -107,7 +122,8 @@ export class CosmosDBBranchDataProvider return new TableAccountResourceItem(accountModel, experience); } - // Unknown experience + // Unknown experience fallback + return new NoSqlAccountResourceItem(accountModel, CoreExperience); } else { // Unknown resource type } diff --git a/src/tree/CosmosDBWorkspaceBranchDataProvider.ts b/src/tree/CosmosDBWorkspaceBranchDataProvider.ts index 5fe67fe8c..6202cd22d 100644 --- a/src/tree/CosmosDBWorkspaceBranchDataProvider.ts +++ b/src/tree/CosmosDBWorkspaceBranchDataProvider.ts @@ -11,11 +11,14 @@ import { } from '@microsoft/vscode-azext-utils'; import { type BranchDataProvider } from '@microsoft/vscode-azureresources-api'; import * as vscode from 'vscode'; +import { API } from '../AzureDBExperiences'; import { ext } from '../extensionVariables'; import { localize } from '../utils/localize'; +import { CosmosDBAttachedAccountsResourceItem } from './attached/CosmosDBAttachedAccountsResourceItem'; import { type CosmosDBResource } from './CosmosAccountModel'; import { type CosmosDBTreeElement } from './CosmosDBTreeElement'; -import { CosmosDBAttachedAccountsResourceItem } from './attached/CosmosDBAttachedAccountsResourceItem'; +import { isTreeElementWithContextValue } from './TreeElementWithContextValue'; +import { isTreeElementWithExperience } from './TreeElementWithExperience'; export class CosmosDBWorkspaceBranchDataProvider extends vscode.Disposable @@ -40,8 +43,24 @@ export class CosmosDBWorkspaceBranchDataProvider 'CosmosDBWorkspaceBranchDataProvider.getChildren', async (context: IActionContext) => { context.telemetry.properties.view = 'workspace'; + context.errorHandling.suppressDisplay = true; + context.errorHandling.rethrow = true; + context.errorHandling.forceIncludeInReportIssueCommand = true; + + if (isTreeElementWithContextValue(element)) { + context.telemetry.properties.parentContext = element.contextValue; + } + + if (isTreeElementWithExperience(element)) { + context.telemetry.properties.experience = element.experience?.api ?? API.Common; + } + + // TODO: values to mask. New TreeElements do not have valueToMask field + // I assume this array should be filled after element.getChildren() call + // And these values should be masked in the context - return (await element.getChildren?.())?.map((child) => { + const children = (await element.getChildren?.()) ?? []; + return children.map((child) => { return ext.state.wrapItemInStateHandling(child, (child: CosmosDBTreeElement) => this.refresh(child), ) as CosmosDBTreeElement; diff --git a/src/tree/TreeElementWithContextValue.ts b/src/tree/TreeElementWithContextValue.ts new file mode 100644 index 000000000..a7bed5d25 --- /dev/null +++ b/src/tree/TreeElementWithContextValue.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export type TreeElementWithContextValue = { + readonly contextValue: string; +}; + +export function isTreeElementWithContextValue(node: unknown): node is TreeElementWithContextValue { + return typeof node === 'object' && node !== null && 'contextValue' in node; +} diff --git a/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts b/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts index 9ed64cb44..c981f0c08 100644 --- a/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts +++ b/src/tree/attached/CosmosDBAttachedAccountsResourceItem.ts @@ -5,9 +5,9 @@ import { callWithTelemetryAndErrorHandling, + createContextValue, createGenericElement, nonNullValue, - type IActionContext, } from '@microsoft/vscode-azext-utils'; import vscode, { ThemeIcon, TreeItemCollapsibleState } from 'vscode'; import { API, getExperienceFromApi } from '../../AzureDBExperiences'; @@ -18,18 +18,22 @@ import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; import { GraphAccountAttachedResourceItem } from '../graph/GraphAccountAttachedResourceItem'; import { NoSqlAccountAttachedResourceItem } from '../nosql/NoSqlAccountAttachedResourceItem'; import { TableAccountAttachedResourceItem } from '../table/TableAccountAttachedResourceItem'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; import { WorkspaceResourceType } from '../workspace/SharedWorkspaceResourceProvider'; import { SharedWorkspaceStorage, type SharedWorkspaceStorageItem } from '../workspace/SharedWorkspaceStorage'; import { type CosmosDBAttachedAccountModel } from './CosmosDBAttachedAccountModel'; -export class CosmosDBAttachedAccountsResourceItem implements CosmosDBTreeElement { - public id: string = WorkspaceResourceType.AttachedAccounts; - public contextValue: string = 'cosmosDB.workspace.item.accounts'; +export class CosmosDBAttachedAccountsResourceItem implements CosmosDBTreeElement, TreeElementWithContextValue { + public readonly id: string = WorkspaceResourceType.AttachedAccounts; + public readonly contextValue: string = 'treeItem.accounts'; private readonly attachDatabaseAccount: CosmosDBTreeElement; private readonly attachEmulator: CosmosDBTreeElement; constructor() { + this.id = WorkspaceResourceType.AttachedAccounts; + this.contextValue = createContextValue([this.contextValue, `attachedAccounts`]); + this.attachDatabaseAccount = createGenericElement({ id: `${this.id}/attachAccount`, contextValue: `${this.contextValue}/attachAccount`, @@ -50,31 +54,20 @@ export class CosmosDBAttachedAccountsResourceItem implements CosmosDBTreeElement } public async getChildren(): Promise { - const items = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.view = 'workspace'; - context.telemetry.properties.parentContext = this.contextValue; - - // TODO: remove after a few releases - await this.migrateV1AccountsToV2(); // Move accounts from the old storage format to the new one - - const items = await SharedWorkspaceStorage.getItems(this.id); - - return await this.getChildrenImpl(items); - }); + // TODO: remove after a few releases + await this.pickSupportedAccounts(); // Move accounts from the old storage format to the new one + const items = await SharedWorkspaceStorage.getItems(this.id); + const children = await this.getChildrenImpl(items); const auxItems = isWindows ? [this.attachDatabaseAccount, this.attachEmulator] : [this.attachDatabaseAccount]; - const result: CosmosDBTreeElement[] = []; - result.push(...(items ?? [])); - result.push(...auxItems); - - return result; + return [...children, ...auxItems]; } public getTreeItem() { return { id: this.id, - contextValue: 'cosmosDB.workspace.item.accounts', + contextValue: this.contextValue, label: 'Attached Database Accounts', iconPath: new ThemeIcon('plug'), collapsibleState: TreeItemCollapsibleState.Collapsed, @@ -120,6 +113,62 @@ export class CosmosDBAttachedAccountsResourceItem implements CosmosDBTreeElement ); } + protected async pickSupportedAccounts(): Promise { + return callWithTelemetryAndErrorHandling( + 'CosmosDBAttachedAccountsResourceItem.pickSupportedAccounts', + async () => { + const serviceName = 'ms-azuretools.vscode-cosmosdb.connectionStrings'; + const value: string | undefined = ext.context.globalState.get(serviceName); + + if (!value) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const accounts: (string | IPersistedAccount)[] = JSON.parse(value); + for (const account of accounts) { + let id: string; + let name: string; + let isEmulator: boolean; + let api: API; + + if (typeof account === 'string') { + // Default to Mongo if the value is a string for the sake of backwards compatibility + // (Mongo was originally the only account type that could be attached) + id = account; + name = account; + api = API.MongoDB; + isEmulator = false; + } else { + id = (account).id; + name = (account).id; + api = (account).defaultExperience; + isEmulator = (account).isEmulator ?? false; + } + + // TODO: Ignore Postgres accounts until we have a way to handle them + if (api === API.PostgresSingle || api === API.PostgresFlexible) { + continue; + } + + const connectionString: string = nonNullValue( + await ext.secretStorage.get(`${serviceName}.${id}`), + 'connectionString', + ); + + const storageItem: SharedWorkspaceStorageItem = { + id, + name, + properties: { isEmulator, api }, + secrets: [connectionString], + }; + + await SharedWorkspaceStorage.push(WorkspaceResourceType.AttachedAccounts, storageItem, true); + } + }, + ); + } + protected async migrateV1AccountsToV2(): Promise { const serviceName = 'ms-azuretools.vscode-cosmosdb.connectionStrings'; const value: string | undefined = ext.context.globalState.get(serviceName); diff --git a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts index 3a62d66b0..93691ae1f 100644 --- a/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountAttachedResourceItem.ts @@ -14,48 +14,30 @@ import { getSignedInPrincipalIdForAccountEndpoint } from '../../docdb/utils/azur import { isRbacException, showRbacPermissionError } from '../../docdb/utils/rbacUtils'; import { localize } from '../../utils/localize'; import { type CosmosDBAttachedAccountModel } from '../attached/CosmosDBAttachedAccountModel'; +import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; import { type AccountInfo } from './AccountInfo'; -export abstract class DocumentDBAccountAttachedResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.workspace.item.account'; +export abstract class DocumentDBAccountAttachedResourceItem extends CosmosAccountResourceItemBase { + public declare readonly account: CosmosDBAttachedAccountModel; // To prevent the RBAC notification from showing up multiple times protected hasShownRbacNotification: boolean = false; - protected constructor( - protected account: CosmosDBAttachedAccountModel, - protected experience: Experience, - ) { - this.contextValue = `${experience.api}.workspace.item.account`; + protected constructor(account: CosmosDBAttachedAccountModel, experience: Experience) { + super(account, experience); } public async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; - - const accountInfo = await this.getAccountInfo(this.account); - const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); - const databases = await this.getDatabases(accountInfo, cosmosClient); - return await this.getChildrenImpl(accountInfo, databases); - }); + const accountInfo = await this.getAccountInfo(this.account); + const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); + const databases = await this.getDatabases(accountInfo, cosmosClient); - return result ?? []; + return this.getChildrenImpl(accountInfo, databases); } public getTreeItem(): TreeItem { - // This function is a bit easier than the ancestor's getTreeItem function - return { - id: this.id, - contextValue: this.contextValue, - iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg'), - label: this.account.name, - description: `(${this.experience.shortName})`, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - }; + return { ...super.getTreeItem(), iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg') }; } protected async getAccountInfo(account: CosmosDBAttachedAccountModel): Promise | never { diff --git a/src/tree/docdb/DocumentDBAccountResourceItem.ts b/src/tree/docdb/DocumentDBAccountResourceItem.ts index 2cd03a0cf..968a1b1f6 100644 --- a/src/tree/docdb/DocumentDBAccountResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountResourceItem.ts @@ -9,6 +9,7 @@ import { type CosmosClient, type DatabaseDefinition, type Resource } from '@azur import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; +import { getThemeAgnosticIconPath } from '../../constants'; import { type CosmosDBCredential, type CosmosDBKeyCredential, getCosmosClient } from '../../docdb/getCosmosClient'; import { getSignedInPrincipalIdForAccountEndpoint } from '../../docdb/utils/azureSessionHelper'; import { ensureRbacPermissionV2, isRbacException, showRbacPermissionError } from '../../docdb/utils/rbacUtils'; @@ -16,54 +17,44 @@ import { createCosmosDBManagementClient } from '../../utils/azureClients'; import { localize } from '../../utils/localize'; import { nonNullProp } from '../../utils/nonNull'; import { type CosmosAccountModel } from '../CosmosAccountModel'; +import { CosmosAccountResourceItemBase } from '../CosmosAccountResourceItemBase'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; import { type AccountInfo } from './AccountInfo'; -export abstract class DocumentDBAccountResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.account'; +export abstract class DocumentDBAccountResourceItem extends CosmosAccountResourceItemBase { + public declare readonly account: CosmosAccountModel; // To prevent the RBAC notification from showing up multiple times protected hasShownRbacNotification: boolean = false; - protected constructor( - protected account: CosmosAccountModel, - protected experience: Experience, - ) { - this.contextValue = `${experience.api}.item.account`; + protected constructor(account: CosmosAccountModel, experience: Experience) { + super(account, experience); } public async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; - - const accountInfo = await this.getAccountInfo(context, this.account); - const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); - const databases = await this.getDatabases(accountInfo, cosmosClient); - return await this.getChildrenImpl(accountInfo, databases); - }); + const accountInfo = await this.getAccountInfo(this.account); + const cosmosClient = getCosmosClient(accountInfo.endpoint, accountInfo.credentials, false); + const databases = await this.getDatabases(accountInfo, cosmosClient); - return result ?? []; + return this.getChildrenImpl(accountInfo, databases); } public getTreeItem(): TreeItem { - // This function is a bit easier than the ancestor's getTreeItem function - return { - id: this.id, - contextValue: this.contextValue, - label: this.account.name, - description: `(${this.experience.shortName})`, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - }; + return { ...super.getTreeItem(), iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg') }; } - protected async getAccountInfo(context: IActionContext, account: CosmosAccountModel): Promise | never { + protected async getAccountInfo(account: CosmosAccountModel): Promise | never { const id = nonNullProp(account, 'id'); const name = nonNullProp(account, 'name'); const resourceGroup = nonNullProp(account, 'resourceGroup'); - const client = await createCosmosDBManagementClient(context, account.subscription); + + const client = await callWithTelemetryAndErrorHandling('getAccountInfo', async (context: IActionContext) => { + return createCosmosDBManagementClient(context, account.subscription); + }); + + if (!client) { + throw new Error('Failed to connect to Cosmos DB account'); + } const databaseAccount = await client.databaseAccounts.get(resourceGroup, name); const credentials = await this.getCredentials(name, resourceGroup, client, databaseAccount); diff --git a/src/tree/docdb/DocumentDBContainerResourceItem.ts b/src/tree/docdb/DocumentDBContainerResourceItem.ts index da22b28a5..7714a786f 100644 --- a/src/tree/docdb/DocumentDBContainerResourceItem.ts +++ b/src/tree/docdb/DocumentDBContainerResourceItem.ts @@ -3,39 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBContainerModel } from './models/DocumentDBContainerModel'; -export abstract class DocumentDBContainerResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.container'; +export abstract class DocumentDBContainerResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.container'; protected constructor( - protected readonly model: DocumentDBContainerModel, - protected readonly experience: Experience, + public readonly model: DocumentDBContainerModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.container`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; + const triggers = await this.getChildrenTriggersImpl(); + const storedProcedures = await this.getChildrenStoredProceduresImpl(); + const items = await this.getChildrenItemsImpl(); - const triggers = await this.getChildrenTriggersImpl(); - const storedProcedures = await this.getChildrenStoredProceduresImpl(); - const items = await this.getChildrenItemsImpl(); - - return [items, storedProcedures, triggers].filter((r) => r !== undefined); - }); - - return result ?? []; + return [items, storedProcedures, triggers].filter((r) => r !== undefined); } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBDatabaseResourceItem.ts b/src/tree/docdb/DocumentDBDatabaseResourceItem.ts index 20f7e561a..c199ab92b 100644 --- a/src/tree/docdb/DocumentDBDatabaseResourceItem.ts +++ b/src/tree/docdb/DocumentDBDatabaseResourceItem.ts @@ -4,40 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import { type ContainerDefinition, type CosmosClient, type Resource } from '@azure/cosmos'; -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { getCosmosClient } from '../../docdb/getCosmosClient'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBDatabaseModel } from './models/DocumentDBDatabaseModel'; -export abstract class DocumentDBDatabaseResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.database'; +export abstract class DocumentDBDatabaseResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.database'; protected constructor( - protected readonly model: DocumentDBDatabaseModel, - protected readonly experience: Experience, + public readonly model: DocumentDBDatabaseModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.database`; + this.id = `${model.accountInfo.id}/${model.database.id}`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + const containers = await this.getContainers(cosmosClient); - const { endpoint, credentials, isEmulator } = this.model.accountInfo; - const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); - const containers = await this.getContainers(cosmosClient); - - return await this.getChildrenImpl(containers); - }); - - return result ?? []; + return this.getChildrenImpl(containers); } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBItemResourceItem.ts b/src/tree/docdb/DocumentDBItemResourceItem.ts index ea06ccd17..8db80c69a 100644 --- a/src/tree/docdb/DocumentDBItemResourceItem.ts +++ b/src/tree/docdb/DocumentDBItemResourceItem.ts @@ -3,26 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { extractPartitionKey, getDocumentId } from '../../utils/document'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBItemModel } from './models/DocumentDBItemModel'; -export abstract class DocumentDBItemResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.item'; +export abstract class DocumentDBItemResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.document'; protected constructor( - protected readonly model: DocumentDBItemModel, - protected readonly experience: Experience, + public readonly model: DocumentDBItemModel, + public readonly experience: Experience, ) { - // Generate a unique ID for the item - // This is used to identify the item in the tree, not the item itself - // The item id is not guaranteed to be unique - this.id = uuid(); - this.contextValue = `${experience.api}.item.item`; + const uniqueId = this.generateUniqueId(this.model); + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/documents/${uniqueId}`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } getTreeItem(): TreeItem { @@ -60,27 +62,49 @@ export abstract class DocumentDBItemResourceItem implements CosmosDBTreeElement if (!this.model.container.partitionKey || this.model.container.partitionKey.paths.length === 0) { return ''; } + const partitionKeyPaths = this.model.container.partitionKey.paths.join(', '); - let partitionKeyValues = extractPartitionKey(this.model.item, this.model.container.partitionKey); - partitionKeyValues = Array.isArray(partitionKeyValues) ? partitionKeyValues : [partitionKeyValues]; - partitionKeyValues = partitionKeyValues.map((v) => { - if (v === null) { - return '\\'; - } - if (v === undefined) { - return '\\'; - } - if (typeof v === 'object') { - return JSON.stringify(v); - } - return v; - }); + const partitionKeyValues = this.generatePartitionKeyValue(this.model); return ( '### Partition Key\n' + '---\n' + `- Paths: **${partitionKeyPaths}**\n` + - `- Values: **${partitionKeyValues.join(', ')}**\n` + `- Values: **${partitionKeyValues}**\n` ); } + + protected generateUniqueId(model: DocumentDBItemModel): string { + const documentId = getDocumentId(model.item, model.container.partitionKey); + const id = documentId?.id; + const rid = documentId?._rid; + const partitionKeyValues = this.generatePartitionKeyValue(model); + + return `${id || ''}|${partitionKeyValues || ''}|${rid || ''}`; + } + + protected generatePartitionKeyValue(model: DocumentDBItemModel): string { + if (!model.container.partitionKey || model.container.partitionKey.paths.length === 0) { + return ''; + } + + let partitionKeyValues = extractPartitionKey(model.item, model.container.partitionKey); + partitionKeyValues = Array.isArray(partitionKeyValues) ? partitionKeyValues : [partitionKeyValues]; + partitionKeyValues = partitionKeyValues + .map((v) => { + if (v === null) { + return '\\'; + } + if (v === undefined) { + return '\\'; + } + if (typeof v === 'object') { + return JSON.stringify(v); + } + return v; + }) + .join(', '); + + return partitionKeyValues; + } } diff --git a/src/tree/docdb/DocumentDBItemsResourceItem.ts b/src/tree/docdb/DocumentDBItemsResourceItem.ts index 89e085c9f..5a1dce80e 100644 --- a/src/tree/docdb/DocumentDBItemsResourceItem.ts +++ b/src/tree/docdb/DocumentDBItemsResourceItem.ts @@ -4,58 +4,51 @@ *--------------------------------------------------------------------------------------------*/ import { type CosmosClient, type FeedOptions, type ItemDefinition, type QueryIterator } from '@azure/cosmos'; -import { - callWithTelemetryAndErrorHandling, - createGenericElement, - type IActionContext, -} from '@microsoft/vscode-azext-utils'; -import { v4 as uuid } from 'uuid'; +import { createContextValue, createGenericElement, type IActionContext } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { getCosmosClient } from '../../docdb/getCosmosClient'; import { getBatchSizeSetting } from '../../utils/workspacUtils'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBItemsModel } from './models/DocumentDBItemsModel'; -export abstract class DocumentDBItemsResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.items'; +export abstract class DocumentDBItemsResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.documents'; protected iterator: QueryIterator | undefined; protected cachedItems: ItemDefinition[] = []; protected hasMoreChildren: boolean = true; protected constructor( - protected readonly model: DocumentDBItemsModel, - protected readonly experience: Experience, + public readonly model: DocumentDBItemsModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.items`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/documents`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } public async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; + if (this.iterator && this.cachedItems.length > 0) { + // ignore + } else { + // Fetch the first batch + const batchSize = getBatchSizeSetting(); + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); - if (this.iterator && this.cachedItems.length > 0) { - // ignore - } else { - // Fetch the first batch - const batchSize = getBatchSizeSetting(); - const { endpoint, credentials, isEmulator } = this.model.accountInfo; - const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + this.iterator = this.getIterator(cosmosClient, { maxItemCount: batchSize }); - this.iterator = this.getIterator(cosmosClient, { maxItemCount: batchSize }); - - await this.getItems(this.iterator); - } + await this.getItems(this.iterator); + } - return await this.getChildrenImpl(this.cachedItems); - }); + const result = await this.getChildrenImpl(this.cachedItems); - if (result && this.hasMoreChildren) { + if (this.hasMoreChildren) { result.push( createGenericElement({ contextValue: this.contextValue, @@ -81,7 +74,7 @@ export abstract class DocumentDBItemsResourceItem implements CosmosDBTreeElement ); } - return result ?? []; + return result; } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts b/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts index 67dcfacd5..8ec5890b8 100644 --- a/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts +++ b/src/tree/docdb/DocumentDBStoredProcedureResourceItem.ts @@ -3,22 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBStoredProcedureModel } from './models/DocumentDBStoredProcedureModel'; -export abstract class DocumentDBStoredProcedureResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.storedProcedure'; +export abstract class DocumentDBStoredProcedureResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.storedProcedure'; protected constructor( - protected readonly model: DocumentDBStoredProcedureModel, - protected readonly experience: Experience, + public readonly model: DocumentDBStoredProcedureModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.storedProcedure`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/storedProcedures/${model.procedure.id}`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts b/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts index 313494a47..36d7d93c8 100644 --- a/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts +++ b/src/tree/docdb/DocumentDBStoredProceduresResourceItem.ts @@ -4,40 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import { type CosmosClient, type Resource, type StoredProcedureDefinition } from '@azure/cosmos'; -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { getCosmosClient } from '../../docdb/getCosmosClient'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBStoredProceduresModel } from './models/DocumentDBStoredProceduresModel'; -export abstract class DocumentDBStoredProceduresResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.storedProcedures'; +export abstract class DocumentDBStoredProceduresResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.storedProcedures'; protected constructor( - protected readonly model: DocumentDBStoredProceduresModel, - protected readonly experience: Experience, + public readonly model: DocumentDBStoredProceduresModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.storedProcedures`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/storedProcedures`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } public async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + const storedProcedures = await this.getStoredProcedures(cosmosClient); - const { endpoint, credentials, isEmulator } = this.model.accountInfo; - const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); - const storedProcedures = await this.getStoredProcedures(cosmosClient); - - return await this.getChildrenImpl(storedProcedures); - }); - - return result ?? []; + return this.getChildrenImpl(storedProcedures); } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBTriggerResourceItem.ts b/src/tree/docdb/DocumentDBTriggerResourceItem.ts index e7f885418..1a3acd125 100644 --- a/src/tree/docdb/DocumentDBTriggerResourceItem.ts +++ b/src/tree/docdb/DocumentDBTriggerResourceItem.ts @@ -3,22 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBTriggerModel } from './models/DocumentDBTriggerModel'; -export abstract class DocumentDBTriggerResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.trigger'; +export abstract class DocumentDBTriggerResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.trigger'; protected constructor( - protected readonly model: DocumentDBTriggerModel, - protected readonly experience: Experience, + public readonly model: DocumentDBTriggerModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.trigger`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/triggers/${model.trigger.id}`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } getTreeItem(): TreeItem { diff --git a/src/tree/docdb/DocumentDBTriggersResourceItem.ts b/src/tree/docdb/DocumentDBTriggersResourceItem.ts index 3adb78907..e3dcd3351 100644 --- a/src/tree/docdb/DocumentDBTriggersResourceItem.ts +++ b/src/tree/docdb/DocumentDBTriggersResourceItem.ts @@ -4,40 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import { type CosmosClient, type Resource, type TriggerDefinition } from '@azure/cosmos'; -import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { v4 as uuid } from 'uuid'; +import { createContextValue } from '@microsoft/vscode-azext-utils'; import vscode, { type TreeItem } from 'vscode'; import { type Experience } from '../../AzureDBExperiences'; import { getCosmosClient } from '../../docdb/getCosmosClient'; import { type CosmosDBTreeElement } from '../CosmosDBTreeElement'; +import { type TreeElementWithContextValue } from '../TreeElementWithContextValue'; +import { type TreeElementWithExperience } from '../TreeElementWithExperience'; import { type DocumentDBTriggersModel } from './models/DocumentDBTriggersModel'; -export abstract class DocumentDBTriggersResourceItem implements CosmosDBTreeElement { - public id: string; - public contextValue: string = 'cosmosDB.item.triggers'; +export abstract class DocumentDBTriggersResourceItem + implements CosmosDBTreeElement, TreeElementWithExperience, TreeElementWithContextValue +{ + public readonly id: string; + public readonly contextValue: string = 'treeItem.triggers'; protected constructor( - protected readonly model: DocumentDBTriggersModel, - protected readonly experience: Experience, + public readonly model: DocumentDBTriggersModel, + public readonly experience: Experience, ) { - this.id = uuid(); - this.contextValue = `${experience.api}.item.triggers`; + this.id = `${model.accountInfo.id}/${model.database.id}/${model.container.id}/triggers`; + this.contextValue = createContextValue([this.contextValue, `experience.${this.experience.api}`]); } public async getChildren(): Promise { - const result = await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => { - context.telemetry.properties.experience = this.experience.api; - context.telemetry.properties.parentContext = this.contextValue; - context.errorHandling.rethrow = true; + const { endpoint, credentials, isEmulator } = this.model.accountInfo; + const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); + const triggers = await this.getTriggers(cosmosClient); - const { endpoint, credentials, isEmulator } = this.model.accountInfo; - const cosmosClient = getCosmosClient(endpoint, credentials, isEmulator); - const triggers = await this.getTriggers(cosmosClient); - - return await this.getChildrenImpl(triggers); - }); - - return result ?? []; + return this.getChildrenImpl(triggers); } getTreeItem(): TreeItem { diff --git a/src/tree/mongo/MongoAccountResourceItem.ts b/src/tree/mongo/MongoAccountResourceItem.ts index ada360b5b..48e4e2eb0 100644 --- a/src/tree/mongo/MongoAccountResourceItem.ts +++ b/src/tree/mongo/MongoAccountResourceItem.ts @@ -27,15 +27,16 @@ import { type MongoAccountModel } from './MongoAccountModel'; // TODO: currently MongoAccountResourceItem does not reuse MongoClusterItemBase, this will be refactored after the v1 to v2 tree migration export class MongoAccountResourceItem extends CosmosAccountResourceItemBase { - declare account: MongoAccountModel; + public declare readonly account: MongoAccountModel; + public readonly contextValue: string = 'treeItem.mongoCluster'; // TODO: this is a bug and overwrites the contextValue from the base class, fix this. constructor( account: MongoAccountModel, - readonly experience: Experience, + experience: Experience, readonly databaseAccount?: DatabaseAccountGetResults, // TODO: exploring during v1->v2 migration readonly isEmulator?: boolean, // TODO: exploring during v1->v2 migration ) { - super(account); + super(account, experience); } async discoverConnectionString(): Promise { diff --git a/src/tree/table/TableAccountAttachedResourceItem.ts b/src/tree/table/TableAccountAttachedResourceItem.ts index b42e68548..6950cb746 100644 --- a/src/tree/table/TableAccountAttachedResourceItem.ts +++ b/src/tree/table/TableAccountAttachedResourceItem.ts @@ -25,9 +25,9 @@ export class TableAccountAttachedResourceItem extends DocumentDBAccountAttachedR return Promise.resolve([ createGenericElement({ - contextValue: this.contextValue, + contextValue: `${this.contextValue}/notSupported`, label: 'Table Accounts are not supported yet.', - id: `${this.id}/no-databases`, + id: `${this.id}/notSupported`, }) as CosmosDBTreeElement, ]); }); diff --git a/src/tree/table/TableAccountResourceItem.ts b/src/tree/table/TableAccountResourceItem.ts index 7cbba8686..262d89f3e 100644 --- a/src/tree/table/TableAccountResourceItem.ts +++ b/src/tree/table/TableAccountResourceItem.ts @@ -25,9 +25,9 @@ export class TableAccountResourceItem extends DocumentDBAccountResourceItem { return Promise.resolve([ createGenericElement({ - contextValue: this.contextValue, + contextValue: `${this.contextValue}/notSupported`, label: 'Table Accounts are not supported yet.', - id: `${this.id}/no-databases`, + id: `${this.id}/notSupported`, }) as CosmosDBTreeElement, ]); }); From a918687ee060de547127dc077a9eb50814fc23ec Mon Sep 17 00:00:00 2001 From: Dmitrii Shilov Date: Thu, 16 Jan 2025 12:31:15 +0100 Subject: [PATCH 36/42] feat: Migrating TreeView to V2 --- extension.bundle.ts | 9 +- src/DatabasesFileSystem.ts | 6 +- .../account/registerAccountCommands.ts | 114 ++++++++++++++ .../DatabaseAccountDeleteStep.ts | 26 ++-- ...izardContext.ts => DeleteWizardContext.ts} | 2 +- .../deleteCosmosDBAccount.ts | 4 +- .../deleteDatabaseAccount.ts | 26 ++-- .../deleteMongoClustersAccount.ts | 4 +- src/docdb/tree/DocDBAccountTreeItemBase.ts | 4 +- src/extension.ts | 128 ++------------- src/extensionVariables.ts | 18 ++- src/mongo/tree/MongoAccountTreeItem.ts | 4 +- src/services/SettingsService.ts | 147 ++++++++++++++++++ src/table/tree/TableAccountTreeItem.ts | 4 +- src/utils/activityUtils.ts | 19 ++- src/utils/settingUtils.ts | 74 --------- 16 files changed, 342 insertions(+), 247 deletions(-) create mode 100644 src/commands/account/registerAccountCommands.ts rename src/commands/deleteDatabaseAccount/{IDeleteWizardContext.ts => DeleteWizardContext.ts} (91%) create mode 100644 src/services/SettingsService.ts delete mode 100644 src/utils/settingUtils.ts diff --git a/extension.bundle.ts b/extension.bundle.ts index 3bb2aefcf..af60a3087 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -24,13 +24,7 @@ export { emulatorPassword, isWindows } from './src/constants'; export { ParsedDocDBConnectionString, parseDocDBConnectionString } from './src/docdb/docDBConnectionStrings'; export { getCosmosClient } from './src/docdb/getCosmosClient'; export * from './src/docdb/registerDocDBCommands'; -export { - activateInternal, - cosmosDBCopyConnectionString, - createServer, - deactivateInternal, - deleteAccount, -} from './src/extension'; +export { activateInternal, deactivateInternal } from './src/extension'; export { ext } from './src/extensionVariables'; export * from './src/graph/registerGraphCommands'; export { connectToMongoClient, isCosmosEmulatorConnectionString } from './src/mongo/connectToMongoClient'; @@ -50,7 +44,6 @@ export * from './src/utils/azureClients'; export { getPublicIpv4, isIpInRanges } from './src/utils/getIp'; export { improveError } from './src/utils/improveError'; export { randomUtils } from './src/utils/randomUtils'; -export { getGlobalSetting, updateGlobalSetting } from './src/utils/settingUtils'; export { rejectOnTimeout, valueOnTimeout } from './src/utils/timeout'; export { IDisposable, getDocumentTreeItemLabel } from './src/utils/vscodeUtils'; export { wrapError } from './src/utils/wrapError'; diff --git a/src/DatabasesFileSystem.ts b/src/DatabasesFileSystem.ts index e58aa0359..830e6956f 100644 --- a/src/DatabasesFileSystem.ts +++ b/src/DatabasesFileSystem.ts @@ -13,8 +13,8 @@ import { import { FileType, workspace, type FileStat, type MessageItem, type Uri } from 'vscode'; import { FileChangeType } from 'vscode-languageclient'; import { ext } from './extensionVariables'; +import { SettingsService } from './services/SettingsService'; import { localize } from './utils/localize'; -import { getWorkspaceSetting, updateGlobalSetting } from './utils/settingUtils'; import { getNodeEditorLabel } from './utils/vscodeUtils'; export interface IEditableTreeItem extends AzExtTreeItem { @@ -50,7 +50,7 @@ export class DatabasesFileSystem extends AzExtTreeFileSystem // NOTE: Using "cosmosDB" instead of "azureDatabases" here for the sake of backwards compatibility. If/when this file system adds support for non-cosmosdb items, we should consider changing this to "azureDatabases" const prefix: string = 'cosmosDB'; const nodeEditorLabel: string = getNodeEditorLabel(node); - if (this._showSaveConfirmation && getWorkspaceSetting(showSavePromptKey, undefined, prefix)) { + if (this._showSaveConfirmation && SettingsService.getSetting(showSavePromptKey, undefined, prefix)) { const message: string = localize( 'saveConfirmation', 'Saving "{0}" will update the entity "{1}" to the cloud.', @@ -65,7 +65,7 @@ export class DatabasesFileSystem extends AzExtTreeFileSystem DialogResponses.dontUpload, ); if (result === DialogResponses.alwaysUpload) { - await updateGlobalSetting(showSavePromptKey, false, prefix); + await SettingsService.updateGlobalSetting(showSavePromptKey, false, prefix); } else if (result === DialogResponses.dontUpload) { throw new UserCancelledError('dontUpload'); } diff --git a/src/commands/account/registerAccountCommands.ts b/src/commands/account/registerAccountCommands.ts new file mode 100644 index 000000000..dfa019d65 --- /dev/null +++ b/src/commands/account/registerAccountCommands.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + type AzExtTreeItem, + type IActionContext, + type ITreeItemPickerContext, + registerCommandWithTreeNodeUnwrapping, +} from '@microsoft/vscode-azext-utils'; +import { platform } from 'os'; +import vscode from 'vscode'; +import { cosmosGremlinFilter, cosmosMongoFilter, cosmosTableFilter, sqlFilter } from '../../constants'; +import { DocDBAccountTreeItem } from '../../docdb/tree/DocDBAccountTreeItem'; +import { type DocDBAccountTreeItemBase } from '../../docdb/tree/DocDBAccountTreeItemBase'; +import { ext } from '../../extensionVariables'; +import { GraphAccountTreeItem } from '../../graph/tree/GraphAccountTreeItem'; +import { setConnectedNode } from '../../mongo/setConnectedNode'; +import { MongoAccountTreeItem } from '../../mongo/tree/MongoAccountTreeItem'; +import { TableAccountTreeItem } from '../../table/tree/TableAccountTreeItem'; +import { AttachedAccountSuffix } from '../../tree/AttachedAccountsTreeItem'; +import { SubscriptionTreeItem } from '../../tree/SubscriptionTreeItem'; +import { localize } from '../../utils/localize'; +import { deleteDatabaseAccount } from '../deleteDatabaseAccount/deleteDatabaseAccount'; + +const cosmosDBTopLevelContextValues: string[] = [ + GraphAccountTreeItem.contextValue, + DocDBAccountTreeItem.contextValue, + TableAccountTreeItem.contextValue, + MongoAccountTreeItem.contextValue, +]; + +export function registerAccountCommands() { + registerCommandWithTreeNodeUnwrapping('azureDatabases.createServer', createServer); + registerCommandWithTreeNodeUnwrapping('cosmosDB.deleteAccount', deleteAccount); + registerCommandWithTreeNodeUnwrapping('cosmosDB.attachDatabaseAccount', async (actionContext: IActionContext) => { + await ext.attachedAccountsNode.attachNewAccount(actionContext); + await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); + }); + registerCommandWithTreeNodeUnwrapping('cosmosDB.attachEmulator', async (actionContext: IActionContext) => { + if (platform() !== 'win32') { + actionContext.errorHandling.suppressReportIssue = true; + throw new Error(localize('emulatorNotSupported', 'The Cosmos DB emulator is only supported on Windows.')); + } + + await ext.attachedAccountsNode.attachEmulator(actionContext); + await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); + }); + registerCommandWithTreeNodeUnwrapping( + 'azureDatabases.detachDatabaseAccount', + async (actionContext: IActionContext & ITreeItemPickerContext, node?: AzExtTreeItem) => { + const children = await ext.attachedAccountsNode.loadAllChildren(actionContext); + if (children.length < 2) { + const message = localize('noAttachedAccounts', 'There are no Attached Accounts.'); + void vscode.window.showInformationMessage(message); + } else { + if (!node) { + node = await ext.rgApi.workspaceResourceTree.showTreeItemPicker( + cosmosDBTopLevelContextValues.map((val: string) => (val += AttachedAccountSuffix)), + actionContext, + ); + } + if (node instanceof MongoAccountTreeItem) { + if (ext.connectedMongoDB && node.fullId === ext.connectedMongoDB.parent.fullId) { + setConnectedNode(undefined); + await node.refresh(actionContext); + } + } + await ext.attachedAccountsNode.detach(node); + await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); + } + }, + ); + registerCommandWithTreeNodeUnwrapping('cosmosDB.copyConnectionString', cosmosDBCopyConnectionString); +} + +export async function createServer(context: IActionContext, node?: SubscriptionTreeItem): Promise { + if (!node) { + node = await ext.rgApi.appResourceTree.showTreeItemPicker( + SubscriptionTreeItem.contextValue, + context, + ); + } + + await SubscriptionTreeItem.createChild(context, node); +} + +export async function deleteAccount(context: IActionContext, node?: AzExtTreeItem): Promise { + const suppressCreateContext: ITreeItemPickerContext = context; + suppressCreateContext.suppressCreatePick = true; + if (!node) { + node = await ext.rgApi.pickAppResource(context, { + filter: [cosmosMongoFilter, cosmosTableFilter, cosmosGremlinFilter, sqlFilter], + }); + } + + await deleteDatabaseAccount(context, node, false); +} + +export async function cosmosDBCopyConnectionString( + context: IActionContext, + node?: MongoAccountTreeItem | DocDBAccountTreeItemBase, +): Promise { + const message = 'The connection string has been copied to the clipboard'; + if (!node) { + node = await ext.rgApi.pickAppResource(context, { + filter: [cosmosMongoFilter, cosmosTableFilter, cosmosGremlinFilter, sqlFilter], + }); + } + + await vscode.env.clipboard.writeText(node.connectionString); + void vscode.window.showInformationMessage(message); +} diff --git a/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts b/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts index 18fd413e0..bf932aabc 100644 --- a/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts +++ b/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts @@ -5,34 +5,34 @@ import { AzExtTreeItem, AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; import { ext } from '../../extensionVariables'; -import { MongoClusterItemBase } from '../../mongoClusters/tree/MongoClusterItemBase'; -import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; +import { MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; -import { type IDeleteWizardContext } from './IDeleteWizardContext'; +import { type DeleteWizardContext } from './DeleteWizardContext'; import { deleteCosmosDBAccount } from './deleteCosmosDBAccount'; import { deleteMongoClustersAccount } from './deleteMongoClustersAccount'; -export class DatabaseAccountDeleteStep extends AzureWizardExecuteStep { +export class DatabaseAccountDeleteStep extends AzureWizardExecuteStep { public priority: number = 100; - public async execute(context: IDeleteWizardContext): Promise { + public async execute(context: DeleteWizardContext): Promise { if (context.node instanceof AzExtTreeItem) { await context.node.deleteTreeItem(context); } else if (context.node instanceof CosmosAccountResourceItemBase) { - await ext.state.showDeleting(context.node.id, async () => { - return deleteCosmosDBAccount(context, context.node as CosmosAccountResourceItemBase); - }); - } else if (context.node instanceof MongoClusterItemBase) { - await ext.state.showDeleting(context.node.id, async () => { - return deleteMongoClustersAccount(context, context.node as MongoClusterResourceItem); - }); + await ext.state.showDeleting(context.node.id, () => + deleteCosmosDBAccount(context, context.node as CosmosAccountResourceItemBase), + ); + ext.cosmosDBBranchDataProvider.refresh(); + } else if (context.node instanceof MongoClusterResourceItem) { + await ext.state.showDeleting(context.node.id, () => + deleteMongoClustersAccount(context, context.node as MongoClusterResourceItem), + ); ext.mongoClustersBranchDataProvider.refresh(); } else { throw new Error('Unexpected node type'); } } - public shouldExecute(_wizardContext: IDeleteWizardContext): boolean { + public shouldExecute(_wizardContext: DeleteWizardContext): boolean { return true; } } diff --git a/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts b/src/commands/deleteDatabaseAccount/DeleteWizardContext.ts similarity index 91% rename from src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts rename to src/commands/deleteDatabaseAccount/DeleteWizardContext.ts index 6e6c91ee8..9bee2832f 100644 --- a/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts +++ b/src/commands/deleteDatabaseAccount/DeleteWizardContext.ts @@ -12,7 +12,7 @@ import { import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; import { type CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; -export interface IDeleteWizardContext extends IActionContext, ExecuteActivityContext { +export interface DeleteWizardContext extends IActionContext, ExecuteActivityContext { node: AzExtTreeItem | CosmosAccountResourceItemBase | MongoClusterResourceItem; deletePostgres: boolean; resourceGroupToDelete?: string; diff --git a/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts b/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts index ccd888e22..d00055341 100644 --- a/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts @@ -13,10 +13,10 @@ import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceI import { createCosmosDBClient } from '../../utils/azureClients'; import { getDatabaseAccountNameFromId } from '../../utils/azureUtils'; import { localize } from '../../utils/localize'; -import { type IDeleteWizardContext } from './IDeleteWizardContext'; +import { type DeleteWizardContext } from './DeleteWizardContext'; export async function deleteCosmosDBAccount( - context: IDeleteWizardContext, + context: DeleteWizardContext, node: AzExtTreeItem | CosmosAccountResourceItemBase, ): Promise { let client: CosmosDBManagementClient; diff --git a/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts b/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts index c4f68f5bc..7bb05d709 100644 --- a/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts @@ -14,10 +14,10 @@ import { import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; import { MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; -import { createActivityContext } from '../../utils/activityUtils'; +import { createActivityContextV2 } from '../../utils/activityUtils'; import { localize } from '../../utils/localize'; import { DatabaseAccountDeleteStep } from './DatabaseAccountDeleteStep'; -import { type IDeleteWizardContext } from './IDeleteWizardContext'; +import { type DeleteWizardContext } from './DeleteWizardContext'; export async function deleteDatabaseAccount( context: IActionContext, @@ -25,39 +25,35 @@ export async function deleteDatabaseAccount( isPostgres: boolean = false, ): Promise { let subscription: ISubscriptionContext; + let accountName: string; if (node instanceof AzExtTreeItem) { subscription = node.subscription; + accountName = node.label; } else if (node instanceof CosmosAccountResourceItemBase && 'subscription' in node.account) { subscription = createSubscriptionContext(node.account.subscription as AzureSubscription); + accountName = node.account.name; } else if (node instanceof MongoClusterResourceItem) { subscription = createSubscriptionContext(node.subscription); + accountName = node.mongoCluster.name; } else { // Not all CosmosAccountResourceItemBase instances have a subscription property (attached account does not), // so we need to create a subscription context throw new Error('Subscription is required to delete an account.'); } - let accountName: string; - if (node instanceof AzExtTreeItem) { - accountName = node.label; - } else if (node instanceof CosmosAccountResourceItemBase) { - accountName = node.account.name; - } else { - accountName = (node as MongoClusterResourceItem).mongoCluster.name; - } - - const wizardContext: IDeleteWizardContext = Object.assign(context, { + const activityContext = await createActivityContextV2(); + const wizardContext: DeleteWizardContext = Object.assign(context, { node, deletePostgres: isPostgres, subscription: subscription, - ...(await createActivityContext()), + ...activityContext, }); - const title = wizardContext.deletePostgres + const title = isPostgres ? localize('deletePoSer', 'Delete Postgres Server "{0}"', accountName) : localize('deleteDbAcc', 'Delete Database Account "{0}"', accountName); - const confirmationMessage = wizardContext.deletePostgres + const confirmationMessage = isPostgres ? localize( 'deleteAccountConfirm', 'Are you sure you want to delete server "{0}" and its contents?', diff --git a/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts b/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts index fa2c92e4f..bdf4dfedc 100644 --- a/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts @@ -8,10 +8,10 @@ import { ext } from '../../extensionVariables'; import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; import { createMongoClustersManagementClient } from '../../utils/azureClients'; import { localize } from '../../utils/localize'; -import { type IDeleteWizardContext } from './IDeleteWizardContext'; +import { type DeleteWizardContext } from './DeleteWizardContext'; export async function deleteMongoClustersAccount( - context: IDeleteWizardContext, + context: DeleteWizardContext, node: MongoClusterResourceItem, ): Promise { const client = createMongoClustersManagementClient(context, node.subscription); diff --git a/src/docdb/tree/DocDBAccountTreeItemBase.ts b/src/docdb/tree/DocDBAccountTreeItemBase.ts index 716da8e40..0e69825cf 100644 --- a/src/docdb/tree/DocDBAccountTreeItemBase.ts +++ b/src/docdb/tree/DocDBAccountTreeItemBase.ts @@ -21,7 +21,7 @@ import { } from '@microsoft/vscode-azext-utils'; import type * as vscode from 'vscode'; import { API } from '../../AzureDBExperiences'; -import { type IDeleteWizardContext } from '../../commands/deleteDatabaseAccount/IDeleteWizardContext'; +import { type DeleteWizardContext } from '../../commands/deleteDatabaseAccount/DeleteWizardContext'; import { deleteCosmosDBAccount } from '../../commands/deleteDatabaseAccount/deleteCosmosDBAccount'; import { getThemeAgnosticIconPath, SERVERLESS_CAPABILITY_NAME } from '../../constants'; import { nonNullProp } from '../../utils/nonNull'; @@ -152,7 +152,7 @@ export abstract class DocDBAccountTreeItemBase extends DocDBTreeItemBase { + public async deleteTreeItemImpl(context: DeleteWizardContext): Promise { await deleteCosmosDBAccount(context, this); } } diff --git a/src/extension.ts b/src/extension.ts index 12225d843..ecc44e09e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,59 +21,36 @@ import { type AzExtParentTreeItem, type AzureExtensionApi, type IActionContext, - type ITreeItemPickerContext, } from '@microsoft/vscode-azext-utils'; +import { type AzureResourcesExtensionApiWithActivity } from '@microsoft/vscode-azext-utils/activity'; import { AzExtResourceType, getAzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api'; -import { platform } from 'os'; import * as vscode from 'vscode'; +import { registerAccountCommands } from './commands/account/registerAccountCommands'; import { findTreeItem } from './commands/api/findTreeItem'; import { pickTreeItem } from './commands/api/pickTreeItem'; import { revealTreeItem } from './commands/api/revealTreeItem'; -import { deleteDatabaseAccount } from './commands/deleteDatabaseAccount/deleteDatabaseAccount'; import { importDocuments } from './commands/importDocuments'; -import { - cosmosGremlinFilter, - cosmosMongoFilter, - cosmosTableFilter, - doubleClickDebounceDelay, - sqlFilter, -} from './constants'; +import { cosmosMongoFilter, doubleClickDebounceDelay, sqlFilter } from './constants'; import { DatabasesFileSystem } from './DatabasesFileSystem'; import { registerDocDBCommands } from './docdb/registerDocDBCommands'; -import { DocDBAccountTreeItem } from './docdb/tree/DocDBAccountTreeItem'; -import { type DocDBAccountTreeItemBase } from './docdb/tree/DocDBAccountTreeItemBase'; import { type DocDBCollectionTreeItem } from './docdb/tree/DocDBCollectionTreeItem'; import { DocDBDocumentTreeItem } from './docdb/tree/DocDBDocumentTreeItem'; import { ext } from './extensionVariables'; import { getResourceGroupsApi } from './getExtensionApi'; import { registerGraphCommands } from './graph/registerGraphCommands'; -import { GraphAccountTreeItem } from './graph/tree/GraphAccountTreeItem'; import { registerMongoCommands } from './mongo/registerMongoCommands'; -import { setConnectedNode } from './mongo/setConnectedNode'; -import { MongoAccountTreeItem } from './mongo/tree/MongoAccountTreeItem'; import { MongoDocumentTreeItem } from './mongo/tree/MongoDocumentTreeItem'; import { MongoClustersExtension } from './mongoClusters/MongoClustersExtension'; import { registerPostgresCommands } from './postgres/commands/registerPostgresCommands'; import { DatabaseResolver } from './resolver/AppResolver'; import { DatabaseWorkspaceProvider } from './resolver/DatabaseWorkspaceProvider'; -import { TableAccountTreeItem } from './table/tree/TableAccountTreeItem'; -import { AttachedAccountSuffix } from './tree/AttachedAccountsTreeItem'; import { CosmosDBBranchDataProvider } from './tree/CosmosDBBranchDataProvider'; import { CosmosDBWorkspaceBranchDataProvider } from './tree/CosmosDBWorkspaceBranchDataProvider'; -import { SubscriptionTreeItem } from './tree/SubscriptionTreeItem'; import { isTreeElementWithExperience } from './tree/TreeElementWithExperience'; import { SharedWorkspaceResourceProvider, WorkspaceResourceType, } from './tree/workspace/SharedWorkspaceResourceProvider'; -import { localize } from './utils/localize'; - -const cosmosDBTopLevelContextValues: string[] = [ - GraphAccountTreeItem.contextValue, - DocDBAccountTreeItem.contextValue, - TableAccountTreeItem.contextValue, - MongoAccountTreeItem.contextValue, -]; export async function activateInternal( context: vscode.ExtensionContext, @@ -100,17 +77,18 @@ export async function activateInternal( // AzureResourceGraph API V1 provided by the getResourceGroupsApi call above. // TreeElementStateManager is needed here too ext.state = new TreeElementStateManager(); - ext.rgApiV2 = await getAzureResourcesExtensionApi(context, '2.0.0'); + ext.rgApiV2 = (await getAzureResourcesExtensionApi(context, '2.0.0')) as AzureResourcesExtensionApiWithActivity; - // ext.rgApi.registerApplicationResourceResolver(AzExtResourceType.AzureCosmosDb, new DatabaseResolver()); + ext.cosmosDBBranchDataProvider = new CosmosDBBranchDataProvider(); + ext.cosmosDBWorkspaceBranchDataProvider = new CosmosDBWorkspaceBranchDataProvider(); ext.rgApiV2.resources.registerAzureResourceBranchDataProvider( AzExtResourceType.AzureCosmosDb, - new CosmosDBBranchDataProvider(), + ext.cosmosDBBranchDataProvider, ); ext.rgApiV2.resources.registerWorkspaceResourceProvider(new SharedWorkspaceResourceProvider()); ext.rgApiV2.resources.registerWorkspaceResourceBranchDataProvider( WorkspaceResourceType.AttachedAccounts, - new CosmosDBWorkspaceBranchDataProvider(), + ext.cosmosDBWorkspaceBranchDataProvider, ); ext.rgApi.registerApplicationResourceResolver( @@ -130,6 +108,7 @@ export async function activateInternal( ext.fileSystem = new DatabasesFileSystem(ext.rgApi.appResourceTree); + registerAccountCommands(); registerDocDBCommands(); registerGraphCommands(); registerPostgresCommands(); @@ -144,30 +123,6 @@ export async function activateInternal( vscode.workspace.registerFileSystemProvider(DatabasesFileSystem.scheme, ext.fileSystem), ); - registerCommandWithTreeNodeUnwrapping('cosmosDB.selectSubscriptions', () => - vscode.commands.executeCommand('azure-account.selectSubscriptions'), - ); - - registerCommandWithTreeNodeUnwrapping('azureDatabases.createServer', createServer); - registerCommandWithTreeNodeUnwrapping('cosmosDB.deleteAccount', deleteAccount); - registerCommandWithTreeNodeUnwrapping( - 'cosmosDB.attachDatabaseAccount', - async (actionContext: IActionContext) => { - await ext.attachedAccountsNode.attachNewAccount(actionContext); - await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); - }, - ); - registerCommandWithTreeNodeUnwrapping('cosmosDB.attachEmulator', async (actionContext: IActionContext) => { - if (platform() !== 'win32') { - actionContext.errorHandling.suppressReportIssue = true; - throw new Error( - localize('emulatorNotSupported', 'The Cosmos DB emulator is only supported on Windows.'), - ); - } - - await ext.attachedAccountsNode.attachEmulator(actionContext); - await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); - }); registerCommandWithTreeNodeUnwrapping( 'azureDatabases.refresh', // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -195,31 +150,6 @@ export async function activateInternal( }, ); - registerCommandWithTreeNodeUnwrapping( - 'azureDatabases.detachDatabaseAccount', - async (actionContext: IActionContext & ITreeItemPickerContext, node?: AzExtTreeItem) => { - const children = await ext.attachedAccountsNode.loadAllChildren(actionContext); - if (children.length < 2) { - const message = localize('noAttachedAccounts', 'There are no Attached Accounts.'); - void vscode.window.showInformationMessage(message); - } else { - if (!node) { - node = await ext.rgApi.workspaceResourceTree.showTreeItemPicker( - cosmosDBTopLevelContextValues.map((val: string) => (val += AttachedAccountSuffix)), - actionContext, - ); - } - if (node instanceof MongoAccountTreeItem) { - if (ext.connectedMongoDB && node.fullId === ext.connectedMongoDB.parent.fullId) { - setConnectedNode(undefined); - await node.refresh(actionContext); - } - } - await ext.attachedAccountsNode.detach(node); - await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); - } - }, - ); registerCommandWithTreeNodeUnwrapping( 'cosmosDB.importDocument', async ( @@ -234,7 +164,7 @@ export async function activateInternal( } }, ); - registerCommandWithTreeNodeUnwrapping('cosmosDB.copyConnectionString', cosmosDBCopyConnectionString); + registerCommandWithTreeNodeUnwrapping( 'cosmosDB.openDocument', async (actionContext: IActionContext, node?: DocDBDocumentTreeItem) => { @@ -294,41 +224,3 @@ export async function activateInternal( export function deactivateInternal(_context: vscode.ExtensionContext): void { // NOOP } - -export async function createServer(context: IActionContext, node?: SubscriptionTreeItem): Promise { - if (!node) { - node = await ext.rgApi.appResourceTree.showTreeItemPicker( - SubscriptionTreeItem.contextValue, - context, - ); - } - - await SubscriptionTreeItem.createChild(context, node); -} - -export async function deleteAccount(context: IActionContext, node?: AzExtTreeItem): Promise { - const suppressCreateContext: ITreeItemPickerContext = context; - suppressCreateContext.suppressCreatePick = true; - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: [cosmosMongoFilter, cosmosTableFilter, cosmosGremlinFilter, sqlFilter], - }); - } - - await deleteDatabaseAccount(context, node, false); -} - -export async function cosmosDBCopyConnectionString( - context: IActionContext, - node?: MongoAccountTreeItem | DocDBAccountTreeItemBase, -): Promise { - const message = 'The connection string has been copied to the clipboard'; - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: [cosmosMongoFilter, cosmosTableFilter, cosmosGremlinFilter, sqlFilter], - }); - } - - await vscode.env.clipboard.writeText(node.connectionString); - void vscode.window.showInformationMessage(message); -} diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 57f6b57ae..f09e5d6bd 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { type IAzExtLogOutputChannel, type TreeElementStateManager } from '@microsoft/vscode-azext-utils'; +import { type AzureResourcesExtensionApiWithActivity } from '@microsoft/vscode-azext-utils/activity'; import { type AzureHostExtensionApi } from '@microsoft/vscode-azext-utils/hostapi'; -import { type AzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api'; import { type ExtensionContext, type SecretStorage } from 'vscode'; import { type DatabasesFileSystem } from './DatabasesFileSystem'; import { type NoSqlCodeLensProvider } from './docdb/NoSqlCodeLensProvider'; @@ -17,6 +17,8 @@ import { type MongoClustersWorkspaceBranchDataProvider } from './mongoClusters/t import { type PostgresCodeLensProvider } from './postgres/services/PostgresCodeLensProvider'; import { type PostgresDatabaseTreeItem } from './postgres/tree/PostgresDatabaseTreeItem'; import { type AttachedAccountsTreeItem } from './tree/AttachedAccountsTreeItem'; +import { type CosmosDBBranchDataProvider } from './tree/CosmosDBBranchDataProvider'; +import { type CosmosDBWorkspaceBranchDataProvider } from './tree/CosmosDBWorkspaceBranchDataProvider'; /** * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts @@ -36,10 +38,22 @@ export namespace ext { export let noSqlCodeLensProvider: NoSqlCodeLensProvider; export let mongoLanguageClient: MongoDBLanguageClient; export let rgApi: AzureHostExtensionApi; - export let rgApiV2: AzureResourcesExtensionApi; + // Since the Azure Resources extension did not update API interface, but added a new interface with activity + // we have to use the new interface AzureResourcesExtensionApiWithActivity instead of AzureResourcesExtensionApi + export let rgApiV2: AzureResourcesExtensionApiWithActivity; export let state: TreeElementStateManager; + // TODO: To avoid these stupid variables the rgApiV2 should have the following public fields (but they are private): + // - AzureResourceProviderManager, + // - AzureResourceBranchDataProviderManager, + // - WorkspaceResourceProviderManager, + // - WorkspaceResourceBranchDataProviderManager, + + // used for the resources tree and the workspace tree REFRESH + export let cosmosDBBranchDataProvider: CosmosDBBranchDataProvider; + export let cosmosDBWorkspaceBranchDataProvider: CosmosDBWorkspaceBranchDataProvider; + // used for the resources tree export let mongoClustersBranchDataProvider: MongoClustersBranchDataProvider; diff --git a/src/mongo/tree/MongoAccountTreeItem.ts b/src/mongo/tree/MongoAccountTreeItem.ts index 49bf08e81..b99092a45 100644 --- a/src/mongo/tree/MongoAccountTreeItem.ts +++ b/src/mongo/tree/MongoAccountTreeItem.ts @@ -16,7 +16,7 @@ import { type MongoClient } from 'mongodb'; import type * as vscode from 'vscode'; import { API } from '../../AzureDBExperiences'; import { deleteCosmosDBAccount } from '../../commands/deleteDatabaseAccount/deleteCosmosDBAccount'; -import { type IDeleteWizardContext } from '../../commands/deleteDatabaseAccount/IDeleteWizardContext'; +import { type DeleteWizardContext } from '../../commands/deleteDatabaseAccount/DeleteWizardContext'; import { getThemeAgnosticIconPath, Links, testDb } from '../../constants'; import { nonNullProp } from '../../utils/nonNull'; import { connectToMongoClient } from '../connectToMongoClient'; @@ -154,7 +154,7 @@ export class MongoAccountTreeItem extends AzExtParentTreeItem { } } - public async deleteTreeItemImpl(context: IDeleteWizardContext): Promise { + public async deleteTreeItemImpl(context: DeleteWizardContext): Promise { await deleteCosmosDBAccount(context, this); } } diff --git a/src/services/SettingsService.ts b/src/services/SettingsService.ts new file mode 100644 index 000000000..55b071f2d --- /dev/null +++ b/src/services/SettingsService.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import path from 'path'; +import { ConfigurationTarget, Uri, workspace, type WorkspaceConfiguration, type WorkspaceFolder } from 'vscode'; +import { ext } from '../extensionVariables'; + +export const vscodeFolder: string = '.vscode'; +export const settingsFile: string = 'settings.json'; + +export class SettingUtils { + /** + * Directly updates one of the user's `Global` configuration settings. + * @param key The key of the setting to update + * @param value The value of the setting to update + * @param prefix The optional extension prefix. Uses ext.prefix unless otherwise specified + */ + async updateGlobalSetting(key: string, value: T, prefix: string = ext.prefix): Promise { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix); + await projectConfiguration.update(key, value, ConfigurationTarget.Global); + } + + /** + * Directly updates one of the user's `Workspace` or `WorkspaceFolder` settings. + * @param key The key of the setting to update + * @param value The value of the setting to update + * @param fsPath The path of the workspace configuration settings + * @param targetSetting The optional workspace setting to target. Uses the `Workspace` configuration target unless otherwise specified + * @param prefix The optional extension prefix. Uses ext.prefix unless otherwise specified + */ + async updateWorkspaceSetting( + key: string, + value: T, + fsPath: string, + targetSetting: + | ConfigurationTarget.Workspace + | ConfigurationTarget.WorkspaceFolder = ConfigurationTarget.Workspace, + prefix: string = ext.prefix, + ): Promise { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix, Uri.file(fsPath)); + await projectConfiguration.update(key, value, targetSetting); + } + + /** + * Directly retrieves one of the user's `Global` configuration settings. + * @param key The key of the setting to retrieve + * @param prefix The optional extension prefix. Uses ext.prefix unless otherwise specified + */ + getGlobalSetting(key: string, prefix: string = ext.prefix): T | undefined { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix); + const result: { globalValue?: T; defaultValue?: T } | undefined = projectConfiguration.inspect(key); + return result?.globalValue === undefined ? result?.defaultValue : result?.globalValue; + } + + /** + * Iteratively retrieves one of the user's workspace settings - sequentially checking for a defined value starting from the `WorkspaceFolder` up to the provided target configuration limit. + * @param key The key of the setting to retrieve + * @param fsPath The optional path of the workspace configuration settings + * @param targetLimit The optional target configuration limit (inclusive). Uses the `Workspace` configuration target unless otherwise specified + * @param prefix The optional extension prefix. Uses ext.prefix unless otherwise specified + */ + getWorkspaceSetting( + key: string, + fsPath?: string, + targetLimit: + | ConfigurationTarget.Workspace + | ConfigurationTarget.WorkspaceFolder = ConfigurationTarget.Workspace, + prefix: string = ext.prefix, + ): T | undefined { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration( + prefix, + fsPath ? Uri.file(fsPath) : undefined, + ); + + const configurationLevel: ConfigurationTarget | undefined = this.getLowestConfigurationLevel( + projectConfiguration, + key, + ); + if (!configurationLevel || (configurationLevel && configurationLevel < targetLimit)) { + return undefined; + } + + return projectConfiguration.get(key); + } + + /** + * Iteratively retrieves one of the user's settings - sequentially checking for a defined value starting from the `WorkspaceFolder` up to the `Global` configuration target. + * @param key The key of the setting to retrieve + * @param fsPath The optional path of the workspace configuration settings + * @param prefix The optional extension prefix. Uses ext.prefix unless otherwise specified + */ + getSetting(key: string, fsPath?: string, prefix: string = ext.prefix): T | undefined { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration( + prefix, + fsPath ? Uri.file(fsPath) : undefined, + ); + return projectConfiguration.get(key); + } + + /** + * Searches through all open folders and gets the current workspace setting (as long as there are no conflicts) + * Uses ext.prefix unless otherwise specified + */ + getWorkspaceSettingFromAnyFolder(key: string, prefix: string = ext.prefix): string | undefined { + if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { + let result: string | undefined; + for (const folder of workspace.workspaceFolders) { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix, folder.uri); + const folderResult: string | undefined = projectConfiguration.get(key); + if (!result) { + result = folderResult; + } else if (folderResult && result !== folderResult) { + return undefined; + } + } + return result; + } else { + return this.getGlobalSetting(key, prefix); + } + } + + getDefaultRootWorkspaceSettingsPath(rootWorkspaceFolder: WorkspaceFolder): string { + return path.join(rootWorkspaceFolder.uri.fsPath, vscodeFolder, settingsFile); + } + + getLowestConfigurationLevel( + projectConfiguration: WorkspaceConfiguration, + key: string, + ): ConfigurationTarget | undefined { + const configuration = projectConfiguration.inspect(key); + + let lowestLevelConfiguration: ConfigurationTarget | undefined; + if (configuration?.workspaceFolderValue !== undefined) { + lowestLevelConfiguration = ConfigurationTarget.WorkspaceFolder; + } else if (configuration?.workspaceValue !== undefined) { + lowestLevelConfiguration = ConfigurationTarget.Workspace; + } else if (configuration?.globalValue !== undefined) { + lowestLevelConfiguration = ConfigurationTarget.Global; + } + + return lowestLevelConfiguration; + } +} + +export const SettingsService = new SettingUtils(); diff --git a/src/table/tree/TableAccountTreeItem.ts b/src/table/tree/TableAccountTreeItem.ts index d387ffe41..9e66aa18d 100644 --- a/src/table/tree/TableAccountTreeItem.ts +++ b/src/table/tree/TableAccountTreeItem.ts @@ -11,7 +11,7 @@ import { } from '@microsoft/vscode-azext-utils'; import { API } from '../../AzureDBExperiences'; import { deleteCosmosDBAccount } from '../../commands/deleteDatabaseAccount/deleteCosmosDBAccount'; -import { type IDeleteWizardContext } from '../../commands/deleteDatabaseAccount/IDeleteWizardContext'; +import { type DeleteWizardContext } from '../../commands/deleteDatabaseAccount/DeleteWizardContext'; import { DocDBAccountTreeItemBase } from '../../docdb/tree/DocDBAccountTreeItemBase'; export class TableAccountTreeItem extends DocDBAccountTreeItemBase { @@ -45,7 +45,7 @@ export class TableAccountTreeItem extends DocDBAccountTreeItemBase { return result ?? []; } - public async deleteTreeItemImpl(context: IDeleteWizardContext): Promise { + public async deleteTreeItemImpl(context: DeleteWizardContext): Promise { await deleteCosmosDBAccount(context, this); } diff --git a/src/utils/activityUtils.ts b/src/utils/activityUtils.ts index 652c0b8d6..a8c12a37a 100644 --- a/src/utils/activityUtils.ts +++ b/src/utils/activityUtils.ts @@ -5,15 +5,28 @@ import { type ExecuteActivityContext } from '@microsoft/vscode-azext-utils'; import { ext } from '../extensionVariables'; -import { getWorkspaceSetting } from './settingUtils'; +import { SettingsService } from '../services/SettingsService'; -export async function createActivityContext(): Promise { +export async function createActivityContext(withChildren?: boolean): Promise { return { registerActivity: async (activity) => ext.rgApi.registerActivity(activity), - suppressNotification: await getWorkspaceSetting( + suppressNotification: await SettingsService.getSetting( 'suppressActivityNotifications', undefined, 'azureResourceGroups', ), + activityChildren: withChildren ? [] : undefined, + }; +} + +export async function createActivityContextV2(withChildren?: boolean): Promise { + return { + registerActivity: async (activity) => ext.rgApiV2.activity.registerActivity(activity), + suppressNotification: await SettingsService.getSetting( + 'suppressActivityNotifications', + undefined, + 'azureResourceGroups', + ), + activityChildren: withChildren ? [] : undefined, }; } diff --git a/src/utils/settingUtils.ts b/src/utils/settingUtils.ts deleted file mode 100644 index af837141d..000000000 --- a/src/utils/settingUtils.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ConfigurationTarget, Uri, workspace, type WorkspaceConfiguration } from 'vscode'; -import { ext } from '../extensionVariables'; - -/** - * Uses ext.prefix 'azureDatabases' unless otherwise specified - */ -export async function updateGlobalSetting( - section: string, - value: T, - prefix: string = ext.prefix, -): Promise { - const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix); - await projectConfiguration.update(section, value, ConfigurationTarget.Global); -} - -/** - * Uses ext.prefix 'azureDatabases' unless otherwise specified - */ -export async function updateWorkspaceSetting( - section: string, - value: T, - fsPath: string, - prefix: string = ext.prefix, -): Promise { - const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix, Uri.file(fsPath)); - await projectConfiguration.update(section, value); -} - -/** - * Uses ext.prefix 'azureDatabases' unless otherwise specified - */ -export function getGlobalSetting(key: string, prefix: string = ext.prefix): T | undefined { - const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix); - const result: { globalValue?: T } | undefined = projectConfiguration.inspect(key); - return result && result.globalValue; -} - -/** - * Uses ext.prefix 'azureDatabases' unless otherwise specified - */ -export function getWorkspaceSetting(key: string, fsPath?: string, prefix: string = ext.prefix): T | undefined { - const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration( - prefix, - fsPath ? Uri.file(fsPath) : undefined, - ); - return projectConfiguration.get(key); -} - -/** - * Searches through all open folders and gets the current workspace setting (as long as there are no conflicts) - * Uses ext.prefix 'azureDatabases' unless otherwise specified - */ -export function getWorkspaceSettingFromAnyFolder(key: string, prefix: string = ext.prefix): string | undefined { - if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { - let result: string | undefined; - for (const folder of workspace.workspaceFolders) { - const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix, folder.uri); - const folderResult: string | undefined = projectConfiguration.get(key); - if (!result) { - result = folderResult; - } else if (folderResult && result !== folderResult) { - return undefined; - } - } - return result; - } else { - return getGlobalSetting(key, prefix); - } -} From 80e56fed5a7d2af54533a0c1ae7733cb1600e8a7 Mon Sep 17 00:00:00 2001 From: Dmitry Shilov Date: Thu, 16 Jan 2025 13:43:24 +0100 Subject: [PATCH 37/42] Dev/sda/tree api migration (#2535) * feat: Migrating TreeView to V2 * feat: Migrating TreeView to V2 * feat: Migrating TreeView to V2 * Updated 'copy connection string' command activation in package.json * feat: Migrating TreeView to V2 --- extension.bundle.ts | 9 +- src/DatabasesFileSystem.ts | 6 +- .../account/registerAccountCommands.ts | 114 ++++++++++++++ .../DatabaseAccountDeleteStep.ts | 26 ++-- ...izardContext.ts => DeleteWizardContext.ts} | 2 +- .../deleteCosmosDBAccount.ts | 4 +- .../deleteDatabaseAccount.ts | 26 ++-- .../deleteMongoClustersAccount.ts | 4 +- src/docdb/tree/DocDBAccountTreeItemBase.ts | 4 +- src/extension.ts | 128 ++------------- src/extensionVariables.ts | 18 ++- src/mongo/tree/MongoAccountTreeItem.ts | 4 +- src/services/SettingsService.ts | 147 ++++++++++++++++++ src/table/tree/TableAccountTreeItem.ts | 4 +- src/utils/activityUtils.ts | 19 ++- src/utils/settingUtils.ts | 74 --------- 16 files changed, 342 insertions(+), 247 deletions(-) create mode 100644 src/commands/account/registerAccountCommands.ts rename src/commands/deleteDatabaseAccount/{IDeleteWizardContext.ts => DeleteWizardContext.ts} (91%) create mode 100644 src/services/SettingsService.ts delete mode 100644 src/utils/settingUtils.ts diff --git a/extension.bundle.ts b/extension.bundle.ts index 3bb2aefcf..af60a3087 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -24,13 +24,7 @@ export { emulatorPassword, isWindows } from './src/constants'; export { ParsedDocDBConnectionString, parseDocDBConnectionString } from './src/docdb/docDBConnectionStrings'; export { getCosmosClient } from './src/docdb/getCosmosClient'; export * from './src/docdb/registerDocDBCommands'; -export { - activateInternal, - cosmosDBCopyConnectionString, - createServer, - deactivateInternal, - deleteAccount, -} from './src/extension'; +export { activateInternal, deactivateInternal } from './src/extension'; export { ext } from './src/extensionVariables'; export * from './src/graph/registerGraphCommands'; export { connectToMongoClient, isCosmosEmulatorConnectionString } from './src/mongo/connectToMongoClient'; @@ -50,7 +44,6 @@ export * from './src/utils/azureClients'; export { getPublicIpv4, isIpInRanges } from './src/utils/getIp'; export { improveError } from './src/utils/improveError'; export { randomUtils } from './src/utils/randomUtils'; -export { getGlobalSetting, updateGlobalSetting } from './src/utils/settingUtils'; export { rejectOnTimeout, valueOnTimeout } from './src/utils/timeout'; export { IDisposable, getDocumentTreeItemLabel } from './src/utils/vscodeUtils'; export { wrapError } from './src/utils/wrapError'; diff --git a/src/DatabasesFileSystem.ts b/src/DatabasesFileSystem.ts index e58aa0359..830e6956f 100644 --- a/src/DatabasesFileSystem.ts +++ b/src/DatabasesFileSystem.ts @@ -13,8 +13,8 @@ import { import { FileType, workspace, type FileStat, type MessageItem, type Uri } from 'vscode'; import { FileChangeType } from 'vscode-languageclient'; import { ext } from './extensionVariables'; +import { SettingsService } from './services/SettingsService'; import { localize } from './utils/localize'; -import { getWorkspaceSetting, updateGlobalSetting } from './utils/settingUtils'; import { getNodeEditorLabel } from './utils/vscodeUtils'; export interface IEditableTreeItem extends AzExtTreeItem { @@ -50,7 +50,7 @@ export class DatabasesFileSystem extends AzExtTreeFileSystem // NOTE: Using "cosmosDB" instead of "azureDatabases" here for the sake of backwards compatibility. If/when this file system adds support for non-cosmosdb items, we should consider changing this to "azureDatabases" const prefix: string = 'cosmosDB'; const nodeEditorLabel: string = getNodeEditorLabel(node); - if (this._showSaveConfirmation && getWorkspaceSetting(showSavePromptKey, undefined, prefix)) { + if (this._showSaveConfirmation && SettingsService.getSetting(showSavePromptKey, undefined, prefix)) { const message: string = localize( 'saveConfirmation', 'Saving "{0}" will update the entity "{1}" to the cloud.', @@ -65,7 +65,7 @@ export class DatabasesFileSystem extends AzExtTreeFileSystem DialogResponses.dontUpload, ); if (result === DialogResponses.alwaysUpload) { - await updateGlobalSetting(showSavePromptKey, false, prefix); + await SettingsService.updateGlobalSetting(showSavePromptKey, false, prefix); } else if (result === DialogResponses.dontUpload) { throw new UserCancelledError('dontUpload'); } diff --git a/src/commands/account/registerAccountCommands.ts b/src/commands/account/registerAccountCommands.ts new file mode 100644 index 000000000..dfa019d65 --- /dev/null +++ b/src/commands/account/registerAccountCommands.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + type AzExtTreeItem, + type IActionContext, + type ITreeItemPickerContext, + registerCommandWithTreeNodeUnwrapping, +} from '@microsoft/vscode-azext-utils'; +import { platform } from 'os'; +import vscode from 'vscode'; +import { cosmosGremlinFilter, cosmosMongoFilter, cosmosTableFilter, sqlFilter } from '../../constants'; +import { DocDBAccountTreeItem } from '../../docdb/tree/DocDBAccountTreeItem'; +import { type DocDBAccountTreeItemBase } from '../../docdb/tree/DocDBAccountTreeItemBase'; +import { ext } from '../../extensionVariables'; +import { GraphAccountTreeItem } from '../../graph/tree/GraphAccountTreeItem'; +import { setConnectedNode } from '../../mongo/setConnectedNode'; +import { MongoAccountTreeItem } from '../../mongo/tree/MongoAccountTreeItem'; +import { TableAccountTreeItem } from '../../table/tree/TableAccountTreeItem'; +import { AttachedAccountSuffix } from '../../tree/AttachedAccountsTreeItem'; +import { SubscriptionTreeItem } from '../../tree/SubscriptionTreeItem'; +import { localize } from '../../utils/localize'; +import { deleteDatabaseAccount } from '../deleteDatabaseAccount/deleteDatabaseAccount'; + +const cosmosDBTopLevelContextValues: string[] = [ + GraphAccountTreeItem.contextValue, + DocDBAccountTreeItem.contextValue, + TableAccountTreeItem.contextValue, + MongoAccountTreeItem.contextValue, +]; + +export function registerAccountCommands() { + registerCommandWithTreeNodeUnwrapping('azureDatabases.createServer', createServer); + registerCommandWithTreeNodeUnwrapping('cosmosDB.deleteAccount', deleteAccount); + registerCommandWithTreeNodeUnwrapping('cosmosDB.attachDatabaseAccount', async (actionContext: IActionContext) => { + await ext.attachedAccountsNode.attachNewAccount(actionContext); + await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); + }); + registerCommandWithTreeNodeUnwrapping('cosmosDB.attachEmulator', async (actionContext: IActionContext) => { + if (platform() !== 'win32') { + actionContext.errorHandling.suppressReportIssue = true; + throw new Error(localize('emulatorNotSupported', 'The Cosmos DB emulator is only supported on Windows.')); + } + + await ext.attachedAccountsNode.attachEmulator(actionContext); + await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); + }); + registerCommandWithTreeNodeUnwrapping( + 'azureDatabases.detachDatabaseAccount', + async (actionContext: IActionContext & ITreeItemPickerContext, node?: AzExtTreeItem) => { + const children = await ext.attachedAccountsNode.loadAllChildren(actionContext); + if (children.length < 2) { + const message = localize('noAttachedAccounts', 'There are no Attached Accounts.'); + void vscode.window.showInformationMessage(message); + } else { + if (!node) { + node = await ext.rgApi.workspaceResourceTree.showTreeItemPicker( + cosmosDBTopLevelContextValues.map((val: string) => (val += AttachedAccountSuffix)), + actionContext, + ); + } + if (node instanceof MongoAccountTreeItem) { + if (ext.connectedMongoDB && node.fullId === ext.connectedMongoDB.parent.fullId) { + setConnectedNode(undefined); + await node.refresh(actionContext); + } + } + await ext.attachedAccountsNode.detach(node); + await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); + } + }, + ); + registerCommandWithTreeNodeUnwrapping('cosmosDB.copyConnectionString', cosmosDBCopyConnectionString); +} + +export async function createServer(context: IActionContext, node?: SubscriptionTreeItem): Promise { + if (!node) { + node = await ext.rgApi.appResourceTree.showTreeItemPicker( + SubscriptionTreeItem.contextValue, + context, + ); + } + + await SubscriptionTreeItem.createChild(context, node); +} + +export async function deleteAccount(context: IActionContext, node?: AzExtTreeItem): Promise { + const suppressCreateContext: ITreeItemPickerContext = context; + suppressCreateContext.suppressCreatePick = true; + if (!node) { + node = await ext.rgApi.pickAppResource(context, { + filter: [cosmosMongoFilter, cosmosTableFilter, cosmosGremlinFilter, sqlFilter], + }); + } + + await deleteDatabaseAccount(context, node, false); +} + +export async function cosmosDBCopyConnectionString( + context: IActionContext, + node?: MongoAccountTreeItem | DocDBAccountTreeItemBase, +): Promise { + const message = 'The connection string has been copied to the clipboard'; + if (!node) { + node = await ext.rgApi.pickAppResource(context, { + filter: [cosmosMongoFilter, cosmosTableFilter, cosmosGremlinFilter, sqlFilter], + }); + } + + await vscode.env.clipboard.writeText(node.connectionString); + void vscode.window.showInformationMessage(message); +} diff --git a/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts b/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts index 18fd413e0..bf932aabc 100644 --- a/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts +++ b/src/commands/deleteDatabaseAccount/DatabaseAccountDeleteStep.ts @@ -5,34 +5,34 @@ import { AzExtTreeItem, AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; import { ext } from '../../extensionVariables'; -import { MongoClusterItemBase } from '../../mongoClusters/tree/MongoClusterItemBase'; -import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; +import { MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; -import { type IDeleteWizardContext } from './IDeleteWizardContext'; +import { type DeleteWizardContext } from './DeleteWizardContext'; import { deleteCosmosDBAccount } from './deleteCosmosDBAccount'; import { deleteMongoClustersAccount } from './deleteMongoClustersAccount'; -export class DatabaseAccountDeleteStep extends AzureWizardExecuteStep { +export class DatabaseAccountDeleteStep extends AzureWizardExecuteStep { public priority: number = 100; - public async execute(context: IDeleteWizardContext): Promise { + public async execute(context: DeleteWizardContext): Promise { if (context.node instanceof AzExtTreeItem) { await context.node.deleteTreeItem(context); } else if (context.node instanceof CosmosAccountResourceItemBase) { - await ext.state.showDeleting(context.node.id, async () => { - return deleteCosmosDBAccount(context, context.node as CosmosAccountResourceItemBase); - }); - } else if (context.node instanceof MongoClusterItemBase) { - await ext.state.showDeleting(context.node.id, async () => { - return deleteMongoClustersAccount(context, context.node as MongoClusterResourceItem); - }); + await ext.state.showDeleting(context.node.id, () => + deleteCosmosDBAccount(context, context.node as CosmosAccountResourceItemBase), + ); + ext.cosmosDBBranchDataProvider.refresh(); + } else if (context.node instanceof MongoClusterResourceItem) { + await ext.state.showDeleting(context.node.id, () => + deleteMongoClustersAccount(context, context.node as MongoClusterResourceItem), + ); ext.mongoClustersBranchDataProvider.refresh(); } else { throw new Error('Unexpected node type'); } } - public shouldExecute(_wizardContext: IDeleteWizardContext): boolean { + public shouldExecute(_wizardContext: DeleteWizardContext): boolean { return true; } } diff --git a/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts b/src/commands/deleteDatabaseAccount/DeleteWizardContext.ts similarity index 91% rename from src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts rename to src/commands/deleteDatabaseAccount/DeleteWizardContext.ts index 6e6c91ee8..9bee2832f 100644 --- a/src/commands/deleteDatabaseAccount/IDeleteWizardContext.ts +++ b/src/commands/deleteDatabaseAccount/DeleteWizardContext.ts @@ -12,7 +12,7 @@ import { import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; import { type CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; -export interface IDeleteWizardContext extends IActionContext, ExecuteActivityContext { +export interface DeleteWizardContext extends IActionContext, ExecuteActivityContext { node: AzExtTreeItem | CosmosAccountResourceItemBase | MongoClusterResourceItem; deletePostgres: boolean; resourceGroupToDelete?: string; diff --git a/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts b/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts index ccd888e22..d00055341 100644 --- a/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteCosmosDBAccount.ts @@ -13,10 +13,10 @@ import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceI import { createCosmosDBClient } from '../../utils/azureClients'; import { getDatabaseAccountNameFromId } from '../../utils/azureUtils'; import { localize } from '../../utils/localize'; -import { type IDeleteWizardContext } from './IDeleteWizardContext'; +import { type DeleteWizardContext } from './DeleteWizardContext'; export async function deleteCosmosDBAccount( - context: IDeleteWizardContext, + context: DeleteWizardContext, node: AzExtTreeItem | CosmosAccountResourceItemBase, ): Promise { let client: CosmosDBManagementClient; diff --git a/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts b/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts index c4f68f5bc..7bb05d709 100644 --- a/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteDatabaseAccount.ts @@ -14,10 +14,10 @@ import { import { type AzureSubscription } from '@microsoft/vscode-azureresources-api'; import { MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; import { CosmosAccountResourceItemBase } from '../../tree/CosmosAccountResourceItemBase'; -import { createActivityContext } from '../../utils/activityUtils'; +import { createActivityContextV2 } from '../../utils/activityUtils'; import { localize } from '../../utils/localize'; import { DatabaseAccountDeleteStep } from './DatabaseAccountDeleteStep'; -import { type IDeleteWizardContext } from './IDeleteWizardContext'; +import { type DeleteWizardContext } from './DeleteWizardContext'; export async function deleteDatabaseAccount( context: IActionContext, @@ -25,39 +25,35 @@ export async function deleteDatabaseAccount( isPostgres: boolean = false, ): Promise { let subscription: ISubscriptionContext; + let accountName: string; if (node instanceof AzExtTreeItem) { subscription = node.subscription; + accountName = node.label; } else if (node instanceof CosmosAccountResourceItemBase && 'subscription' in node.account) { subscription = createSubscriptionContext(node.account.subscription as AzureSubscription); + accountName = node.account.name; } else if (node instanceof MongoClusterResourceItem) { subscription = createSubscriptionContext(node.subscription); + accountName = node.mongoCluster.name; } else { // Not all CosmosAccountResourceItemBase instances have a subscription property (attached account does not), // so we need to create a subscription context throw new Error('Subscription is required to delete an account.'); } - let accountName: string; - if (node instanceof AzExtTreeItem) { - accountName = node.label; - } else if (node instanceof CosmosAccountResourceItemBase) { - accountName = node.account.name; - } else { - accountName = (node as MongoClusterResourceItem).mongoCluster.name; - } - - const wizardContext: IDeleteWizardContext = Object.assign(context, { + const activityContext = await createActivityContextV2(); + const wizardContext: DeleteWizardContext = Object.assign(context, { node, deletePostgres: isPostgres, subscription: subscription, - ...(await createActivityContext()), + ...activityContext, }); - const title = wizardContext.deletePostgres + const title = isPostgres ? localize('deletePoSer', 'Delete Postgres Server "{0}"', accountName) : localize('deleteDbAcc', 'Delete Database Account "{0}"', accountName); - const confirmationMessage = wizardContext.deletePostgres + const confirmationMessage = isPostgres ? localize( 'deleteAccountConfirm', 'Are you sure you want to delete server "{0}" and its contents?', diff --git a/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts b/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts index fa2c92e4f..bdf4dfedc 100644 --- a/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts +++ b/src/commands/deleteDatabaseAccount/deleteMongoClustersAccount.ts @@ -8,10 +8,10 @@ import { ext } from '../../extensionVariables'; import { type MongoClusterResourceItem } from '../../mongoClusters/tree/MongoClusterResourceItem'; import { createMongoClustersManagementClient } from '../../utils/azureClients'; import { localize } from '../../utils/localize'; -import { type IDeleteWizardContext } from './IDeleteWizardContext'; +import { type DeleteWizardContext } from './DeleteWizardContext'; export async function deleteMongoClustersAccount( - context: IDeleteWizardContext, + context: DeleteWizardContext, node: MongoClusterResourceItem, ): Promise { const client = createMongoClustersManagementClient(context, node.subscription); diff --git a/src/docdb/tree/DocDBAccountTreeItemBase.ts b/src/docdb/tree/DocDBAccountTreeItemBase.ts index 716da8e40..0e69825cf 100644 --- a/src/docdb/tree/DocDBAccountTreeItemBase.ts +++ b/src/docdb/tree/DocDBAccountTreeItemBase.ts @@ -21,7 +21,7 @@ import { } from '@microsoft/vscode-azext-utils'; import type * as vscode from 'vscode'; import { API } from '../../AzureDBExperiences'; -import { type IDeleteWizardContext } from '../../commands/deleteDatabaseAccount/IDeleteWizardContext'; +import { type DeleteWizardContext } from '../../commands/deleteDatabaseAccount/DeleteWizardContext'; import { deleteCosmosDBAccount } from '../../commands/deleteDatabaseAccount/deleteCosmosDBAccount'; import { getThemeAgnosticIconPath, SERVERLESS_CAPABILITY_NAME } from '../../constants'; import { nonNullProp } from '../../utils/nonNull'; @@ -152,7 +152,7 @@ export abstract class DocDBAccountTreeItemBase extends DocDBTreeItemBase { + public async deleteTreeItemImpl(context: DeleteWizardContext): Promise { await deleteCosmosDBAccount(context, this); } } diff --git a/src/extension.ts b/src/extension.ts index 12225d843..ecc44e09e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,59 +21,36 @@ import { type AzExtParentTreeItem, type AzureExtensionApi, type IActionContext, - type ITreeItemPickerContext, } from '@microsoft/vscode-azext-utils'; +import { type AzureResourcesExtensionApiWithActivity } from '@microsoft/vscode-azext-utils/activity'; import { AzExtResourceType, getAzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api'; -import { platform } from 'os'; import * as vscode from 'vscode'; +import { registerAccountCommands } from './commands/account/registerAccountCommands'; import { findTreeItem } from './commands/api/findTreeItem'; import { pickTreeItem } from './commands/api/pickTreeItem'; import { revealTreeItem } from './commands/api/revealTreeItem'; -import { deleteDatabaseAccount } from './commands/deleteDatabaseAccount/deleteDatabaseAccount'; import { importDocuments } from './commands/importDocuments'; -import { - cosmosGremlinFilter, - cosmosMongoFilter, - cosmosTableFilter, - doubleClickDebounceDelay, - sqlFilter, -} from './constants'; +import { cosmosMongoFilter, doubleClickDebounceDelay, sqlFilter } from './constants'; import { DatabasesFileSystem } from './DatabasesFileSystem'; import { registerDocDBCommands } from './docdb/registerDocDBCommands'; -import { DocDBAccountTreeItem } from './docdb/tree/DocDBAccountTreeItem'; -import { type DocDBAccountTreeItemBase } from './docdb/tree/DocDBAccountTreeItemBase'; import { type DocDBCollectionTreeItem } from './docdb/tree/DocDBCollectionTreeItem'; import { DocDBDocumentTreeItem } from './docdb/tree/DocDBDocumentTreeItem'; import { ext } from './extensionVariables'; import { getResourceGroupsApi } from './getExtensionApi'; import { registerGraphCommands } from './graph/registerGraphCommands'; -import { GraphAccountTreeItem } from './graph/tree/GraphAccountTreeItem'; import { registerMongoCommands } from './mongo/registerMongoCommands'; -import { setConnectedNode } from './mongo/setConnectedNode'; -import { MongoAccountTreeItem } from './mongo/tree/MongoAccountTreeItem'; import { MongoDocumentTreeItem } from './mongo/tree/MongoDocumentTreeItem'; import { MongoClustersExtension } from './mongoClusters/MongoClustersExtension'; import { registerPostgresCommands } from './postgres/commands/registerPostgresCommands'; import { DatabaseResolver } from './resolver/AppResolver'; import { DatabaseWorkspaceProvider } from './resolver/DatabaseWorkspaceProvider'; -import { TableAccountTreeItem } from './table/tree/TableAccountTreeItem'; -import { AttachedAccountSuffix } from './tree/AttachedAccountsTreeItem'; import { CosmosDBBranchDataProvider } from './tree/CosmosDBBranchDataProvider'; import { CosmosDBWorkspaceBranchDataProvider } from './tree/CosmosDBWorkspaceBranchDataProvider'; -import { SubscriptionTreeItem } from './tree/SubscriptionTreeItem'; import { isTreeElementWithExperience } from './tree/TreeElementWithExperience'; import { SharedWorkspaceResourceProvider, WorkspaceResourceType, } from './tree/workspace/SharedWorkspaceResourceProvider'; -import { localize } from './utils/localize'; - -const cosmosDBTopLevelContextValues: string[] = [ - GraphAccountTreeItem.contextValue, - DocDBAccountTreeItem.contextValue, - TableAccountTreeItem.contextValue, - MongoAccountTreeItem.contextValue, -]; export async function activateInternal( context: vscode.ExtensionContext, @@ -100,17 +77,18 @@ export async function activateInternal( // AzureResourceGraph API V1 provided by the getResourceGroupsApi call above. // TreeElementStateManager is needed here too ext.state = new TreeElementStateManager(); - ext.rgApiV2 = await getAzureResourcesExtensionApi(context, '2.0.0'); + ext.rgApiV2 = (await getAzureResourcesExtensionApi(context, '2.0.0')) as AzureResourcesExtensionApiWithActivity; - // ext.rgApi.registerApplicationResourceResolver(AzExtResourceType.AzureCosmosDb, new DatabaseResolver()); + ext.cosmosDBBranchDataProvider = new CosmosDBBranchDataProvider(); + ext.cosmosDBWorkspaceBranchDataProvider = new CosmosDBWorkspaceBranchDataProvider(); ext.rgApiV2.resources.registerAzureResourceBranchDataProvider( AzExtResourceType.AzureCosmosDb, - new CosmosDBBranchDataProvider(), + ext.cosmosDBBranchDataProvider, ); ext.rgApiV2.resources.registerWorkspaceResourceProvider(new SharedWorkspaceResourceProvider()); ext.rgApiV2.resources.registerWorkspaceResourceBranchDataProvider( WorkspaceResourceType.AttachedAccounts, - new CosmosDBWorkspaceBranchDataProvider(), + ext.cosmosDBWorkspaceBranchDataProvider, ); ext.rgApi.registerApplicationResourceResolver( @@ -130,6 +108,7 @@ export async function activateInternal( ext.fileSystem = new DatabasesFileSystem(ext.rgApi.appResourceTree); + registerAccountCommands(); registerDocDBCommands(); registerGraphCommands(); registerPostgresCommands(); @@ -144,30 +123,6 @@ export async function activateInternal( vscode.workspace.registerFileSystemProvider(DatabasesFileSystem.scheme, ext.fileSystem), ); - registerCommandWithTreeNodeUnwrapping('cosmosDB.selectSubscriptions', () => - vscode.commands.executeCommand('azure-account.selectSubscriptions'), - ); - - registerCommandWithTreeNodeUnwrapping('azureDatabases.createServer', createServer); - registerCommandWithTreeNodeUnwrapping('cosmosDB.deleteAccount', deleteAccount); - registerCommandWithTreeNodeUnwrapping( - 'cosmosDB.attachDatabaseAccount', - async (actionContext: IActionContext) => { - await ext.attachedAccountsNode.attachNewAccount(actionContext); - await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); - }, - ); - registerCommandWithTreeNodeUnwrapping('cosmosDB.attachEmulator', async (actionContext: IActionContext) => { - if (platform() !== 'win32') { - actionContext.errorHandling.suppressReportIssue = true; - throw new Error( - localize('emulatorNotSupported', 'The Cosmos DB emulator is only supported on Windows.'), - ); - } - - await ext.attachedAccountsNode.attachEmulator(actionContext); - await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); - }); registerCommandWithTreeNodeUnwrapping( 'azureDatabases.refresh', // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -195,31 +150,6 @@ export async function activateInternal( }, ); - registerCommandWithTreeNodeUnwrapping( - 'azureDatabases.detachDatabaseAccount', - async (actionContext: IActionContext & ITreeItemPickerContext, node?: AzExtTreeItem) => { - const children = await ext.attachedAccountsNode.loadAllChildren(actionContext); - if (children.length < 2) { - const message = localize('noAttachedAccounts', 'There are no Attached Accounts.'); - void vscode.window.showInformationMessage(message); - } else { - if (!node) { - node = await ext.rgApi.workspaceResourceTree.showTreeItemPicker( - cosmosDBTopLevelContextValues.map((val: string) => (val += AttachedAccountSuffix)), - actionContext, - ); - } - if (node instanceof MongoAccountTreeItem) { - if (ext.connectedMongoDB && node.fullId === ext.connectedMongoDB.parent.fullId) { - setConnectedNode(undefined); - await node.refresh(actionContext); - } - } - await ext.attachedAccountsNode.detach(node); - await ext.rgApi.workspaceResourceTree.refresh(actionContext, ext.attachedAccountsNode); - } - }, - ); registerCommandWithTreeNodeUnwrapping( 'cosmosDB.importDocument', async ( @@ -234,7 +164,7 @@ export async function activateInternal( } }, ); - registerCommandWithTreeNodeUnwrapping('cosmosDB.copyConnectionString', cosmosDBCopyConnectionString); + registerCommandWithTreeNodeUnwrapping( 'cosmosDB.openDocument', async (actionContext: IActionContext, node?: DocDBDocumentTreeItem) => { @@ -294,41 +224,3 @@ export async function activateInternal( export function deactivateInternal(_context: vscode.ExtensionContext): void { // NOOP } - -export async function createServer(context: IActionContext, node?: SubscriptionTreeItem): Promise { - if (!node) { - node = await ext.rgApi.appResourceTree.showTreeItemPicker( - SubscriptionTreeItem.contextValue, - context, - ); - } - - await SubscriptionTreeItem.createChild(context, node); -} - -export async function deleteAccount(context: IActionContext, node?: AzExtTreeItem): Promise { - const suppressCreateContext: ITreeItemPickerContext = context; - suppressCreateContext.suppressCreatePick = true; - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: [cosmosMongoFilter, cosmosTableFilter, cosmosGremlinFilter, sqlFilter], - }); - } - - await deleteDatabaseAccount(context, node, false); -} - -export async function cosmosDBCopyConnectionString( - context: IActionContext, - node?: MongoAccountTreeItem | DocDBAccountTreeItemBase, -): Promise { - const message = 'The connection string has been copied to the clipboard'; - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: [cosmosMongoFilter, cosmosTableFilter, cosmosGremlinFilter, sqlFilter], - }); - } - - await vscode.env.clipboard.writeText(node.connectionString); - void vscode.window.showInformationMessage(message); -} diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 57f6b57ae..f09e5d6bd 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { type IAzExtLogOutputChannel, type TreeElementStateManager } from '@microsoft/vscode-azext-utils'; +import { type AzureResourcesExtensionApiWithActivity } from '@microsoft/vscode-azext-utils/activity'; import { type AzureHostExtensionApi } from '@microsoft/vscode-azext-utils/hostapi'; -import { type AzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api'; import { type ExtensionContext, type SecretStorage } from 'vscode'; import { type DatabasesFileSystem } from './DatabasesFileSystem'; import { type NoSqlCodeLensProvider } from './docdb/NoSqlCodeLensProvider'; @@ -17,6 +17,8 @@ import { type MongoClustersWorkspaceBranchDataProvider } from './mongoClusters/t import { type PostgresCodeLensProvider } from './postgres/services/PostgresCodeLensProvider'; import { type PostgresDatabaseTreeItem } from './postgres/tree/PostgresDatabaseTreeItem'; import { type AttachedAccountsTreeItem } from './tree/AttachedAccountsTreeItem'; +import { type CosmosDBBranchDataProvider } from './tree/CosmosDBBranchDataProvider'; +import { type CosmosDBWorkspaceBranchDataProvider } from './tree/CosmosDBWorkspaceBranchDataProvider'; /** * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts @@ -36,10 +38,22 @@ export namespace ext { export let noSqlCodeLensProvider: NoSqlCodeLensProvider; export let mongoLanguageClient: MongoDBLanguageClient; export let rgApi: AzureHostExtensionApi; - export let rgApiV2: AzureResourcesExtensionApi; + // Since the Azure Resources extension did not update API interface, but added a new interface with activity + // we have to use the new interface AzureResourcesExtensionApiWithActivity instead of AzureResourcesExtensionApi + export let rgApiV2: AzureResourcesExtensionApiWithActivity; export let state: TreeElementStateManager; + // TODO: To avoid these stupid variables the rgApiV2 should have the following public fields (but they are private): + // - AzureResourceProviderManager, + // - AzureResourceBranchDataProviderManager, + // - WorkspaceResourceProviderManager, + // - WorkspaceResourceBranchDataProviderManager, + + // used for the resources tree and the workspace tree REFRESH + export let cosmosDBBranchDataProvider: CosmosDBBranchDataProvider; + export let cosmosDBWorkspaceBranchDataProvider: CosmosDBWorkspaceBranchDataProvider; + // used for the resources tree export let mongoClustersBranchDataProvider: MongoClustersBranchDataProvider; diff --git a/src/mongo/tree/MongoAccountTreeItem.ts b/src/mongo/tree/MongoAccountTreeItem.ts index 49bf08e81..b99092a45 100644 --- a/src/mongo/tree/MongoAccountTreeItem.ts +++ b/src/mongo/tree/MongoAccountTreeItem.ts @@ -16,7 +16,7 @@ import { type MongoClient } from 'mongodb'; import type * as vscode from 'vscode'; import { API } from '../../AzureDBExperiences'; import { deleteCosmosDBAccount } from '../../commands/deleteDatabaseAccount/deleteCosmosDBAccount'; -import { type IDeleteWizardContext } from '../../commands/deleteDatabaseAccount/IDeleteWizardContext'; +import { type DeleteWizardContext } from '../../commands/deleteDatabaseAccount/DeleteWizardContext'; import { getThemeAgnosticIconPath, Links, testDb } from '../../constants'; import { nonNullProp } from '../../utils/nonNull'; import { connectToMongoClient } from '../connectToMongoClient'; @@ -154,7 +154,7 @@ export class MongoAccountTreeItem extends AzExtParentTreeItem { } } - public async deleteTreeItemImpl(context: IDeleteWizardContext): Promise { + public async deleteTreeItemImpl(context: DeleteWizardContext): Promise { await deleteCosmosDBAccount(context, this); } } diff --git a/src/services/SettingsService.ts b/src/services/SettingsService.ts new file mode 100644 index 000000000..55b071f2d --- /dev/null +++ b/src/services/SettingsService.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import path from 'path'; +import { ConfigurationTarget, Uri, workspace, type WorkspaceConfiguration, type WorkspaceFolder } from 'vscode'; +import { ext } from '../extensionVariables'; + +export const vscodeFolder: string = '.vscode'; +export const settingsFile: string = 'settings.json'; + +export class SettingUtils { + /** + * Directly updates one of the user's `Global` configuration settings. + * @param key The key of the setting to update + * @param value The value of the setting to update + * @param prefix The optional extension prefix. Uses ext.prefix unless otherwise specified + */ + async updateGlobalSetting(key: string, value: T, prefix: string = ext.prefix): Promise { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix); + await projectConfiguration.update(key, value, ConfigurationTarget.Global); + } + + /** + * Directly updates one of the user's `Workspace` or `WorkspaceFolder` settings. + * @param key The key of the setting to update + * @param value The value of the setting to update + * @param fsPath The path of the workspace configuration settings + * @param targetSetting The optional workspace setting to target. Uses the `Workspace` configuration target unless otherwise specified + * @param prefix The optional extension prefix. Uses ext.prefix unless otherwise specified + */ + async updateWorkspaceSetting( + key: string, + value: T, + fsPath: string, + targetSetting: + | ConfigurationTarget.Workspace + | ConfigurationTarget.WorkspaceFolder = ConfigurationTarget.Workspace, + prefix: string = ext.prefix, + ): Promise { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix, Uri.file(fsPath)); + await projectConfiguration.update(key, value, targetSetting); + } + + /** + * Directly retrieves one of the user's `Global` configuration settings. + * @param key The key of the setting to retrieve + * @param prefix The optional extension prefix. Uses ext.prefix unless otherwise specified + */ + getGlobalSetting(key: string, prefix: string = ext.prefix): T | undefined { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix); + const result: { globalValue?: T; defaultValue?: T } | undefined = projectConfiguration.inspect(key); + return result?.globalValue === undefined ? result?.defaultValue : result?.globalValue; + } + + /** + * Iteratively retrieves one of the user's workspace settings - sequentially checking for a defined value starting from the `WorkspaceFolder` up to the provided target configuration limit. + * @param key The key of the setting to retrieve + * @param fsPath The optional path of the workspace configuration settings + * @param targetLimit The optional target configuration limit (inclusive). Uses the `Workspace` configuration target unless otherwise specified + * @param prefix The optional extension prefix. Uses ext.prefix unless otherwise specified + */ + getWorkspaceSetting( + key: string, + fsPath?: string, + targetLimit: + | ConfigurationTarget.Workspace + | ConfigurationTarget.WorkspaceFolder = ConfigurationTarget.Workspace, + prefix: string = ext.prefix, + ): T | undefined { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration( + prefix, + fsPath ? Uri.file(fsPath) : undefined, + ); + + const configurationLevel: ConfigurationTarget | undefined = this.getLowestConfigurationLevel( + projectConfiguration, + key, + ); + if (!configurationLevel || (configurationLevel && configurationLevel < targetLimit)) { + return undefined; + } + + return projectConfiguration.get(key); + } + + /** + * Iteratively retrieves one of the user's settings - sequentially checking for a defined value starting from the `WorkspaceFolder` up to the `Global` configuration target. + * @param key The key of the setting to retrieve + * @param fsPath The optional path of the workspace configuration settings + * @param prefix The optional extension prefix. Uses ext.prefix unless otherwise specified + */ + getSetting(key: string, fsPath?: string, prefix: string = ext.prefix): T | undefined { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration( + prefix, + fsPath ? Uri.file(fsPath) : undefined, + ); + return projectConfiguration.get(key); + } + + /** + * Searches through all open folders and gets the current workspace setting (as long as there are no conflicts) + * Uses ext.prefix unless otherwise specified + */ + getWorkspaceSettingFromAnyFolder(key: string, prefix: string = ext.prefix): string | undefined { + if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { + let result: string | undefined; + for (const folder of workspace.workspaceFolders) { + const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix, folder.uri); + const folderResult: string | undefined = projectConfiguration.get(key); + if (!result) { + result = folderResult; + } else if (folderResult && result !== folderResult) { + return undefined; + } + } + return result; + } else { + return this.getGlobalSetting(key, prefix); + } + } + + getDefaultRootWorkspaceSettingsPath(rootWorkspaceFolder: WorkspaceFolder): string { + return path.join(rootWorkspaceFolder.uri.fsPath, vscodeFolder, settingsFile); + } + + getLowestConfigurationLevel( + projectConfiguration: WorkspaceConfiguration, + key: string, + ): ConfigurationTarget | undefined { + const configuration = projectConfiguration.inspect(key); + + let lowestLevelConfiguration: ConfigurationTarget | undefined; + if (configuration?.workspaceFolderValue !== undefined) { + lowestLevelConfiguration = ConfigurationTarget.WorkspaceFolder; + } else if (configuration?.workspaceValue !== undefined) { + lowestLevelConfiguration = ConfigurationTarget.Workspace; + } else if (configuration?.globalValue !== undefined) { + lowestLevelConfiguration = ConfigurationTarget.Global; + } + + return lowestLevelConfiguration; + } +} + +export const SettingsService = new SettingUtils(); diff --git a/src/table/tree/TableAccountTreeItem.ts b/src/table/tree/TableAccountTreeItem.ts index d387ffe41..9e66aa18d 100644 --- a/src/table/tree/TableAccountTreeItem.ts +++ b/src/table/tree/TableAccountTreeItem.ts @@ -11,7 +11,7 @@ import { } from '@microsoft/vscode-azext-utils'; import { API } from '../../AzureDBExperiences'; import { deleteCosmosDBAccount } from '../../commands/deleteDatabaseAccount/deleteCosmosDBAccount'; -import { type IDeleteWizardContext } from '../../commands/deleteDatabaseAccount/IDeleteWizardContext'; +import { type DeleteWizardContext } from '../../commands/deleteDatabaseAccount/DeleteWizardContext'; import { DocDBAccountTreeItemBase } from '../../docdb/tree/DocDBAccountTreeItemBase'; export class TableAccountTreeItem extends DocDBAccountTreeItemBase { @@ -45,7 +45,7 @@ export class TableAccountTreeItem extends DocDBAccountTreeItemBase { return result ?? []; } - public async deleteTreeItemImpl(context: IDeleteWizardContext): Promise { + public async deleteTreeItemImpl(context: DeleteWizardContext): Promise { await deleteCosmosDBAccount(context, this); } diff --git a/src/utils/activityUtils.ts b/src/utils/activityUtils.ts index 652c0b8d6..a8c12a37a 100644 --- a/src/utils/activityUtils.ts +++ b/src/utils/activityUtils.ts @@ -5,15 +5,28 @@ import { type ExecuteActivityContext } from '@microsoft/vscode-azext-utils'; import { ext } from '../extensionVariables'; -import { getWorkspaceSetting } from './settingUtils'; +import { SettingsService } from '../services/SettingsService'; -export async function createActivityContext(): Promise { +export async function createActivityContext(withChildren?: boolean): Promise { return { registerActivity: async (activity) => ext.rgApi.registerActivity(activity), - suppressNotification: await getWorkspaceSetting( + suppressNotification: await SettingsService.getSetting( 'suppressActivityNotifications', undefined, 'azureResourceGroups', ), + activityChildren: withChildren ? [] : undefined, + }; +} + +export async function createActivityContextV2(withChildren?: boolean): Promise { + return { + registerActivity: async (activity) => ext.rgApiV2.activity.registerActivity(activity), + suppressNotification: await SettingsService.getSetting( + 'suppressActivityNotifications', + undefined, + 'azureResourceGroups', + ), + activityChildren: withChildren ? [] : undefined, }; } diff --git a/src/utils/settingUtils.ts b/src/utils/settingUtils.ts deleted file mode 100644 index af837141d..000000000 --- a/src/utils/settingUtils.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ConfigurationTarget, Uri, workspace, type WorkspaceConfiguration } from 'vscode'; -import { ext } from '../extensionVariables'; - -/** - * Uses ext.prefix 'azureDatabases' unless otherwise specified - */ -export async function updateGlobalSetting( - section: string, - value: T, - prefix: string = ext.prefix, -): Promise { - const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix); - await projectConfiguration.update(section, value, ConfigurationTarget.Global); -} - -/** - * Uses ext.prefix 'azureDatabases' unless otherwise specified - */ -export async function updateWorkspaceSetting( - section: string, - value: T, - fsPath: string, - prefix: string = ext.prefix, -): Promise { - const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix, Uri.file(fsPath)); - await projectConfiguration.update(section, value); -} - -/** - * Uses ext.prefix 'azureDatabases' unless otherwise specified - */ -export function getGlobalSetting(key: string, prefix: string = ext.prefix): T | undefined { - const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix); - const result: { globalValue?: T } | undefined = projectConfiguration.inspect(key); - return result && result.globalValue; -} - -/** - * Uses ext.prefix 'azureDatabases' unless otherwise specified - */ -export function getWorkspaceSetting(key: string, fsPath?: string, prefix: string = ext.prefix): T | undefined { - const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration( - prefix, - fsPath ? Uri.file(fsPath) : undefined, - ); - return projectConfiguration.get(key); -} - -/** - * Searches through all open folders and gets the current workspace setting (as long as there are no conflicts) - * Uses ext.prefix 'azureDatabases' unless otherwise specified - */ -export function getWorkspaceSettingFromAnyFolder(key: string, prefix: string = ext.prefix): string | undefined { - if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { - let result: string | undefined; - for (const folder of workspace.workspaceFolders) { - const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix, folder.uri); - const folderResult: string | undefined = projectConfiguration.get(key); - if (!result) { - result = folderResult; - } else if (folderResult && result !== folderResult) { - return undefined; - } - } - return result; - } else { - return getGlobalSetting(key, prefix); - } -} From d15ed0aff1d0ccd999dde1636a64a9ab110d5aef Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Jan 2025 17:25:03 +0100 Subject: [PATCH 38/42] feat: "copy connection string" for account nodes --- package.json | 7 +- src/commands/account/copyConnectionString.ts | 72 +++++++++++++++++++ .../account/registerAccountCommands.ts | 19 +---- src/mongoClusters/MongoClustersExtension.ts | 5 -- .../commands/copyConnectionString.ts | 49 ------------- .../docdb/DocumentDBAccountResourceItem.ts | 15 ++++ 6 files changed, 90 insertions(+), 77 deletions(-) create mode 100644 src/commands/account/copyConnectionString.ts delete mode 100644 src/mongoClusters/commands/copyConnectionString.ts diff --git a/package.json b/package.json index 1c538f142..39bac9c9b 100644 --- a/package.json +++ b/package.json @@ -584,11 +584,6 @@ "command": "command.mongoClusters.importDocuments", "title": "Import Documents into Collection..." }, - { - "category": "MongoDB Clusters", - "command": "command.mongoClusters.copyConnectionString", - "title": "Copy Connection String" - }, { "category": "MongoDB Clusters", "command": "command.mongoClusters.exportDocuments", @@ -844,7 +839,7 @@ }, { "//": "[Account] Copy connection string to Mongo Cluster or MongoDB (RU) account", - "command": "command.mongoClusters.copyConnectionString", + "command": "cosmosDB.copyConnectionString", "when": "view =~ /azure(ResourceGroups|FocusView|Workspace)/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i", "group": "2@1" }, diff --git a/src/commands/account/copyConnectionString.ts b/src/commands/account/copyConnectionString.ts new file mode 100644 index 000000000..41972173c --- /dev/null +++ b/src/commands/account/copyConnectionString.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { ext } from '../../extensionVariables'; +import { MongoClusterItemBase } from '../../mongoClusters/tree/MongoClusterItemBase'; +import { type CosmosDBAttachedAccountsResourceItem } from '../../tree/attached/CosmosDBAttachedAccountsResourceItem'; +import { DocumentDBAccountAttachedResourceItem } from '../../tree/docdb/DocumentDBAccountAttachedResourceItem'; +import { DocumentDBAccountResourceItem } from '../../tree/docdb/DocumentDBAccountResourceItem'; +import { MongoAccountResourceItem } from '../../tree/mongo/MongoAccountResourceItem'; +import { localize } from '../../utils/localize'; + +export async function copyConnectionString( + context: IActionContext, + node?: + | DocumentDBAccountAttachedResourceItem // NoSQL and other DocuemntDB accounts (except Mongo RU) in the resource area + | CosmosDBAttachedAccountsResourceItem // NoSQL and other DocumentDB accounts (except Mongo RU) in the workspace area + | MongoAccountResourceItem // Mongo (RU), WIP/work in progress, currently only the resource area + | MongoClusterItemBase, // Mongo Cluster (vCore), in buth, the resource and in the workspace area +): Promise { + if (!node) { + throw new Error('WIP: No node selected.'); // wip, wire up a picker + // node = await ext.rgApi.pickAppResource(context, { + // filter: [cosmosMongoFilter, cosmosTableFilter, cosmosGremlinFilter, sqlFilter], + // }); + } + + const connectionString = await ext.state.runWithTemporaryDescription( + node.id as string, // wip, temporary hack for v1 + localize('copyConnectionString.working', 'Working...'), + async () => { + if (node instanceof DocumentDBAccountResourceItem) { + context.telemetry.properties.experience = node.experience.api; + return await node.getConnectionString(); + } + + if (node instanceof MongoAccountResourceItem) { + context.telemetry.properties.experience = node.experience.api; + return node.discoverConnectionString(); + } + + if (node instanceof DocumentDBAccountAttachedResourceItem) { + context.telemetry.properties.experience = node.experience.api; + return node.account.connectionString; + } + + if (node instanceof MongoClusterItemBase) { + context.telemetry.properties.experience = node.mongoCluster.dbExperience?.api; + return node.discoverConnectionString(); + } + + return undefined; + }, + ); + + if (!connectionString) { + void vscode.window.showErrorMessage( + localize( + 'copyConnectionString.noConnectionString', + 'Failed to extract the connection string from the selected account.', + ), + ); + } else { + await vscode.env.clipboard.writeText(connectionString); + void vscode.window.showInformationMessage( + localize('copyConnectionString.success', 'The connection string has been copied to the clipboard'), + ); + } +} diff --git a/src/commands/account/registerAccountCommands.ts b/src/commands/account/registerAccountCommands.ts index dfa019d65..7492ec272 100644 --- a/src/commands/account/registerAccountCommands.ts +++ b/src/commands/account/registerAccountCommands.ts @@ -13,7 +13,6 @@ import { platform } from 'os'; import vscode from 'vscode'; import { cosmosGremlinFilter, cosmosMongoFilter, cosmosTableFilter, sqlFilter } from '../../constants'; import { DocDBAccountTreeItem } from '../../docdb/tree/DocDBAccountTreeItem'; -import { type DocDBAccountTreeItemBase } from '../../docdb/tree/DocDBAccountTreeItemBase'; import { ext } from '../../extensionVariables'; import { GraphAccountTreeItem } from '../../graph/tree/GraphAccountTreeItem'; import { setConnectedNode } from '../../mongo/setConnectedNode'; @@ -23,6 +22,7 @@ import { AttachedAccountSuffix } from '../../tree/AttachedAccountsTreeItem'; import { SubscriptionTreeItem } from '../../tree/SubscriptionTreeItem'; import { localize } from '../../utils/localize'; import { deleteDatabaseAccount } from '../deleteDatabaseAccount/deleteDatabaseAccount'; +import { copyConnectionString } from './copyConnectionString'; const cosmosDBTopLevelContextValues: string[] = [ GraphAccountTreeItem.contextValue, @@ -72,7 +72,7 @@ export function registerAccountCommands() { } }, ); - registerCommandWithTreeNodeUnwrapping('cosmosDB.copyConnectionString', cosmosDBCopyConnectionString); + registerCommandWithTreeNodeUnwrapping('cosmosDB.copyConnectionString', copyConnectionString); } export async function createServer(context: IActionContext, node?: SubscriptionTreeItem): Promise { @@ -97,18 +97,3 @@ export async function deleteAccount(context: IActionContext, node?: AzExtTreeIte await deleteDatabaseAccount(context, node, false); } - -export async function cosmosDBCopyConnectionString( - context: IActionContext, - node?: MongoAccountTreeItem | DocDBAccountTreeItemBase, -): Promise { - const message = 'The connection string has been copied to the clipboard'; - if (!node) { - node = await ext.rgApi.pickAppResource(context, { - filter: [cosmosMongoFilter, cosmosTableFilter, cosmosGremlinFilter, sqlFilter], - }); - } - - await vscode.env.clipboard.writeText(node.connectionString); - void vscode.window.showInformationMessage(message); -} diff --git a/src/mongoClusters/MongoClustersExtension.ts b/src/mongoClusters/MongoClustersExtension.ts index 246e55937..967209a98 100644 --- a/src/mongoClusters/MongoClustersExtension.ts +++ b/src/mongoClusters/MongoClustersExtension.ts @@ -20,7 +20,6 @@ import * as vscode from 'vscode'; import { ext } from '../extensionVariables'; import { WorkspaceResourceType } from '../tree/workspace/SharedWorkspaceResourceProvider'; import { addWorkspaceConnection } from './commands/addWorkspaceConnection'; -import { copyConnectionString } from './commands/copyConnectionString'; import { createCollection } from './commands/createCollection'; import { createDatabase } from './commands/createDatabase'; import { createDocument } from './commands/createDocument'; @@ -99,10 +98,6 @@ export class MongoClustersExtension implements vscode.Disposable { registerCommand('command.internal.mongoClusters.documentView.open', openDocumentView); registerCommandWithTreeNodeUnwrapping('command.mongoClusters.launchShell', launchShell); - registerCommandWithTreeNodeUnwrapping( - 'command.mongoClusters.copyConnectionString', - copyConnectionString, - ); registerCommandWithTreeNodeUnwrapping('command.mongoClusters.dropCollection', dropCollection); registerCommandWithTreeNodeUnwrapping('command.mongoClusters.dropDatabase', dropDatabase); diff --git a/src/mongoClusters/commands/copyConnectionString.ts b/src/mongoClusters/commands/copyConnectionString.ts deleted file mode 100644 index da9985e68..000000000 --- a/src/mongoClusters/commands/copyConnectionString.ts +++ /dev/null @@ -1,49 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type IActionContext } from '@microsoft/vscode-azext-utils'; -import * as vscode from 'vscode'; -import { ext } from '../../extensionVariables'; -import { MongoAccountResourceItem } from '../../tree/mongo/MongoAccountResourceItem'; -import { localize } from '../../utils/localize'; -import { MongoClusterItemBase } from '../tree/MongoClusterItemBase'; - -export async function copyConnectionString( - context: IActionContext, - clusterNode?: MongoClusterItemBase | MongoAccountResourceItem, -): Promise { - // node ??= ... pick a node if not provided - if (!clusterNode) { - throw new Error('No cluster selected.'); - } - - const connectionString = await ext.state.runWithTemporaryDescription(clusterNode.id, 'Working...', async () => { - if (clusterNode instanceof MongoAccountResourceItem) { - context.telemetry.properties.experience = clusterNode.experience.api; - return clusterNode.discoverConnectionString(); - } - - if (clusterNode instanceof MongoClusterItemBase) { - context.telemetry.properties.experience = clusterNode.mongoCluster.dbExperience?.api; - return clusterNode.discoverConnectionString(); - } - - return undefined; - }); - - if (!connectionString) { - void vscode.window.showErrorMessage( - localize( - 'copyConnectionString.noConnectionString', - 'Failed to extract the connection string from the selected cluster.', - ), - ); - } else { - await vscode.env.clipboard.writeText(connectionString); - void vscode.window.showInformationMessage( - localize('copyConnectionString.success', 'The connection string has been copied to the clipboard'), - ); - } -} diff --git a/src/tree/docdb/DocumentDBAccountResourceItem.ts b/src/tree/docdb/DocumentDBAccountResourceItem.ts index 968a1b1f6..7950de41b 100644 --- a/src/tree/docdb/DocumentDBAccountResourceItem.ts +++ b/src/tree/docdb/DocumentDBAccountResourceItem.ts @@ -43,6 +43,21 @@ export abstract class DocumentDBAccountResourceItem extends CosmosAccountResourc return { ...super.getTreeItem(), iconPath: getThemeAgnosticIconPath('CosmosDBAccount.svg') }; } + public async getConnectionString(): Promise { + const accountInfo = await this.getAccountInfo(this.account); + + // supporting only one known success path + if ( + accountInfo.credentials.length === 2 && + accountInfo.credentials[0].type === 'key' && + accountInfo.credentials[1].type === 'auth' + ) { + return `AccountEndpoint=${accountInfo.endpoint};AccountKey=${accountInfo.credentials[0].key}`; + } else { + return undefined; + } + } + protected async getAccountInfo(account: CosmosAccountModel): Promise | never { const id = nonNullProp(account, 'id'); const name = nonNullProp(account, 'name'); From 3c580f56cbb4527b85540bcc5752144d2e11d354 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 17 Jan 2025 12:39:33 +0100 Subject: [PATCH 39/42] feat: "copy connection string" for account nodes --- src/commands/account/copyConnectionString.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/account/copyConnectionString.ts b/src/commands/account/copyConnectionString.ts index 41972173c..652160dcc 100644 --- a/src/commands/account/copyConnectionString.ts +++ b/src/commands/account/copyConnectionString.ts @@ -29,7 +29,7 @@ export async function copyConnectionString( } const connectionString = await ext.state.runWithTemporaryDescription( - node.id as string, // wip, temporary hack for v1 + node.id, localize('copyConnectionString.working', 'Working...'), async () => { if (node instanceof DocumentDBAccountResourceItem) { From 66d2ca4e17e9edc6be4dc3b96f253ae81d94f290 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 17 Jan 2025 12:53:09 +0100 Subject: [PATCH 40/42] fix: build errors after SettingUtils update --- extension.bundle.ts | 5 +++-- test/runWithSetting.ts | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/extension.bundle.ts b/extension.bundle.ts index af60a3087..6405cf5e0 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -32,20 +32,21 @@ export { MongoCommand } from './src/mongo/MongoCommand'; export { addDatabaseToAccountConnectionString, encodeMongoConnectionString, - getDatabaseNameFromConnectionString, + getDatabaseNameFromConnectionString } from './src/mongo/mongoConnectionStrings'; export { findCommandAtPosition, getAllCommandsFromText } from './src/mongo/MongoScrapbook'; export { MongoShell } from './src/mongo/MongoShell'; export * from './src/mongo/registerMongoCommands'; export { IDatabaseInfo } from './src/mongo/tree/MongoAccountTreeItem'; export { addDatabaseToConnectionString } from './src/postgres/postgresConnectionStrings'; +export { SettingUtils } from './src/services/SettingsService'; export { AttachedAccountsTreeItem, MONGO_CONNECTION_EXPECTED } from './src/tree/AttachedAccountsTreeItem'; export * from './src/utils/azureClients'; export { getPublicIpv4, isIpInRanges } from './src/utils/getIp'; export { improveError } from './src/utils/improveError'; export { randomUtils } from './src/utils/randomUtils'; export { rejectOnTimeout, valueOnTimeout } from './src/utils/timeout'; -export { IDisposable, getDocumentTreeItemLabel } from './src/utils/vscodeUtils'; +export { getDocumentTreeItemLabel, IDisposable } from './src/utils/vscodeUtils'; export { wrapError } from './src/utils/wrapError'; // NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen diff --git a/test/runWithSetting.ts b/test/runWithSetting.ts index eb0e5df71..97d42b537 100644 --- a/test/runWithSetting.ts +++ b/test/runWithSetting.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ext, getGlobalSetting, updateGlobalSetting } from '../extension.bundle'; +import { ext } from '../extension.bundle'; +import { SettingsService } from '../src/services/SettingsService'; export async function runWithDatabasesSetting( key: string, @@ -27,11 +28,11 @@ async function runWithSettingInternal( prefix: string, callback: () => Promise, ): Promise { - const oldValue: string | boolean | undefined = getGlobalSetting(key, prefix); + const oldValue: string | boolean | undefined = SettingsService.getGlobalSetting(key, prefix); try { - await updateGlobalSetting(key, value, prefix); + await SettingsService.updateGlobalSetting(key, value, prefix); await callback(); } finally { - await updateGlobalSetting(key, oldValue, prefix); + await SettingsService.updateGlobalSetting(key, oldValue, prefix); } } From 90d1b74a2d8c22541c5c489aabd1614964d224fc Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 17 Jan 2025 12:55:06 +0100 Subject: [PATCH 41/42] feat: "copy connection string" for account nodes --- src/commands/account/copyConnectionString.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/account/copyConnectionString.ts b/src/commands/account/copyConnectionString.ts index 652160dcc..a44efaf33 100644 --- a/src/commands/account/copyConnectionString.ts +++ b/src/commands/account/copyConnectionString.ts @@ -42,6 +42,8 @@ export async function copyConnectionString( return node.discoverConnectionString(); } + // TODO: revisit when updating "Attached Accounts" storage and migration: runWithTemporaryDescription was not showing the temporary description + // most likely due to a mismatching node.id. if (node instanceof DocumentDBAccountAttachedResourceItem) { context.telemetry.properties.experience = node.experience.api; return node.account.connectionString; From 31538a95212b222a3a702f0b7ee8d0a539547ace Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Fri, 17 Jan 2025 14:23:46 +0100 Subject: [PATCH 42/42] fix: post-merge build error --- src/commands/account/registerAccountCommands.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/account/registerAccountCommands.ts b/src/commands/account/registerAccountCommands.ts index b67162a67..7492ec272 100644 --- a/src/commands/account/registerAccountCommands.ts +++ b/src/commands/account/registerAccountCommands.ts @@ -13,7 +13,6 @@ import { platform } from 'os'; import vscode from 'vscode'; import { cosmosGremlinFilter, cosmosMongoFilter, cosmosTableFilter, sqlFilter } from '../../constants'; import { DocDBAccountTreeItem } from '../../docdb/tree/DocDBAccountTreeItem'; -import { type DocDBAccountTreeItemBase } from '../../docdb/tree/DocDBAccountTreeItemBase'; import { ext } from '../../extensionVariables'; import { GraphAccountTreeItem } from '../../graph/tree/GraphAccountTreeItem'; import { setConnectedNode } from '../../mongo/setConnectedNode';